C++

538 阅读18分钟

Before:粗略地看了一遍翁恺的 C++ 课程,然后在这里看面试总结

  • 预处理:头文件、宏定义插入与替换
  • 编译:语法分析...翻译成汇编代码(Java 是字节码)
  • 汇编:汇编语言转为机器语言
  • 链接:将有关的目标文件和库文件彼此连接

m.elecfans.com/article/663…
www.cnblogs.com/magicsoar/p…

面向对象与面向过程

面向过程:以步骤划分问题
面向对象:以功能划分问题

以上课的过程作为例子:

  • 面向过程:同学们走进教室坐下来,老师走进来,铃声响了,老师开始讲话,同学开始听课……
  • 面向对象:上课要有教室,要有一个老师,要有很多学生,或者有电脑,他们有很多属性(功能)和关系,比如老师可以发出声音,学生可以记笔记,电脑可以放课件

内存管理

一个由 C/C++ 编译的程序占用的内存分为以下几个部分:

  1. 栈区(stack)— 由编译器自动分配释放,存放函数的参数值局部变量的值等
  2. 堆区(heap) — 由程序员分配和释放,若程序员不释放,程序结束时可能由 OS 回收
  3. 全局区 / 静态区存储区(static)— 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量、未初始化的静态变量在相邻的另一块区域
  4. 文字常量区 — 常量字符串就是放在这里的
  5. 程序代码区 — 存放函数体的二进制代码

内联函数(编译时期展开函数)

interview.huihut.com/#/?id=inlin…

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

虚函数可以是内联函数,但是当虚函数表现多态性的时候不能内联,因为虚函数表现多态是在运行期绑定的。当编译器知道所调用的对象是哪个类(如 Base::who())才行

struct / class

C 和 C++ 中 struct 的区别

首先,在面向 C 过程中,这里的 struct 是一种数据类型,那么里面肯定不能定义函数,否则报错,C++ 可以包含函数

struct 和 class 的区别

  • 默认访问权限和默认继承权限不同,前者 public 后者 private
  • class 可用于定义模板参数,而 struct 不可以

一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值:blog.csdn.net/firefly_200…

内存对齐

按最宽数据类型对齐(如果下一个元素能摆下,就继续放;摆不下,就从头开始摆。下方例子)

优点:内存对齐主要是为了提高程序的性能,数据结构特别是栈,应尽可能的在自然边界上对齐,经对齐后 CPU 的内存访问速度大大提升(未对齐可能需要两次访存)

// 按 4 对齐
class Data {
    char c;
    // int 摆不下了,要从头开始
    int a;
    char d;
};
cout << sizeof(Data) << endl;   12

class Data {
    char c;
    // 第二个 char 仍然能摆下
    char d;
    int a;
};
cout << sizeof(Data) << endl;   8

static

  1. 修饰普通变量,修改变量的存储区域和生命周期(延长生命周期),使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

consts

  • 修饰变量,修饰指针
  • 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改
  • 修饰成员函数,说明该成员函数内不能修改成员变量

const 可用于对重载函数的区分

this

  1. this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象
  2. 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。
  3. 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
  4. this 指针被隐含地声明为: ClassName *const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
  5. this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
  6. 在以下场景中,经常需要显式引用 this 指针:
    1. 为实现对象的链式引用;
    2. 为避免对同一对象进行赋值操作;
    3. 在实现一些数据结构时,如 list

指针常量和常量指针

const 后边的内容为“常量”,英文更好理解
blog.csdn.net/qq_36132127…

int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // * 在前,定义为指针常量
  • 常量指针(pointer to const / 指向常量的指针):不能修改指向地址的内容
int main()
{
    int a = 2;
    int const *b = &a;
    // 报错
    *b = 3;
    printf("albert:%d\n",a);
}
  • 指针常量(const pointer):指针指向的地址不可以修改,但内容可以改变
int main()
{
    int a = 2;
    int b = 3;
    int *const c = &a;
    printf("albert:%p\n", c);
    // 报错
    c = &b;
    printf("albert:%p\n",c);
}

多态、动态绑定(和 Java 不同)

多态可以分为静态多态动态多态,所谓静态多态就是通过函数重载、模板、强制类型转换实现的,静态多态是在函数编译阶段就决定调用的机制,即在编译链接截断将函数的入口地址给出,而动态多态是在程序运行时刻才决定调用机制,而在 C++ 中动态多态是通过虚函数实现的(还有一些注意点看这里

  • 通过 virtual 动态绑定,要通过指针或引用?
A a;
B b;
a = b;

这样是不行的,因为 b 的 vtable( 有几个基类就有几个虚函数表) 没有赋给 a(对象无法访问虚函数表),而是要 A *a = &b

虚函数的调用关系:this -> vptr -> vtable -> virtual function

  • 父类指针或引用指向子类对象

  • 析构函数也要 virtual,不然只调用父类的析构

  • Java 默认就动态绑定

纯虚函数(Java 接口)、虚继承等

具体:interview.huihut.com/#/?id=纯虚函数

  • 虚函数指针(占用类的空间)、虚函数表(不占用类的空间):songlee24.github.io/2014/09/02/…

  • 虚继承:用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。虚基类指针(表)

  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类

  • 抽象类含有纯虚函数,接口类仅含有纯虚函数

拷贝构造函数

www.cnblogs.com/jingqinglin…

拷贝构造函数的调用时机

  1. 函数的参数为类的对象
  2. 函数的返回值是类的对象
  3. 对象需要通过另外一个对象进行初始化

默认拷贝构造函数仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值(指针则是复制地址)。默认拷贝构造函数没有处理静态成员变量

浅拷贝、深拷贝

浅拷贝

在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,出现错误

深拷贝

此时 rect1 的 p 和 rect2 的 p 各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是“深拷贝”

参数为什么必须是引用

为了防止递归引用

一个对象需要以值方式传递时,编译器会调用它的拷贝构造函数以生成一个复本。若拷贝构造函数里的参数也是值传递,那么会继续调用拷贝构造函数以生成一个复本,继而陷入递归

静态变量

静态成员变量的初始化不能省略

class A { 
public: 
	//声明但未定义
    static int a;
 }; 
//此处定义了静态成员变量,同时初始化,不能省略
int A::a = 3;
int main() { 
    printf("%d", A::a);
    return 0;
}
 

重载运算符

class Integer {
public:
    Integet(int n = 0) : i(n) {}
    // 若没有第一个 const 则返回值可以作为左值,类似:1 = a
    // 后两个 const 代表两个操作数不能被修改
    const Integer operator+(const Integer &n) const {
    	return Integer(n.i + i);
    }

private:
int i;
};
  • 类型转换

重载运算符和拷贝构造函数都可以实现

上述代码中 f(a) 会出错,因为写了两种类型转换的方法,编译器不知道用哪个。可以通过在拷贝构造函数前加上 explicit 来关闭隐式类型转换

volatile

volatile int i = 10; 
  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
  • const 可以是 volatile (如只读的状态寄存器)
  • 指针可以是 volatile

friend 友元类和友元函数

  • 能访问私有成员
  • 破坏封装性
  • 友元关系不可传递、具有单向性
  • 友元声明的形式及数量不受限制
  • 友元函数没有 this 指针

例子:www.runoob.com/cplusplus/c…

new、delete

  • new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)

  • delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间

  • 🚀new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数

  • 用 free 来释放 new 出来的东西会发生什么?
    答:对于简单数据类型,和 delete 一样;对对象来说,free 不会调用析构函数

C++ 函数调用的过程

blog.csdn.net/HDong99/art…
www.cnblogs.com/sddai/p/976…

  • 为什么参数是从右到左入栈的?
    因为存在不定长参数的函数,如 printf,编译器通过 format 参数中的 % 占位符的个数来确定参数的个数,现在我们假设参数的压栈顺序是从左到右的,由于 format 先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到 format,而要找到 format,必须要知道参数的个数。

  • 再问,不是有 EBP 栈底指针吗?
    看这张图,

    参数是属于调用者的栈帧

四种强制类型转换

dynamic_cast

  • 用于多态类型的转换
  • 执行运行时类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
  • 可以在整个类层次结构中移动指针,包括向上转换、向下转换

static_cast

  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

const_cast

用于删除 const 和 volatile 关键字

reinterpret_cast

  • 允许将任何指针转换为任何其他指针类型(如 char*int*One_class*Unrelated_class* 之类的转换,但其本身并不安全)
  • 也允许将任何整数类型转换为任何指针类型以及反向转换

静态库和动态库区别

静态库:在链接阶段,会将汇编生成的目标文件 .o 与引用到的库一起链接打包到可执行文件中。

静态链接器主要完成以下两个任务:

  • 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来
  • 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置

静态库缺点:

  • 空间浪费。多个程序都用到某个静态库,则静态库在内存存在多份拷贝
  • 如果静态库 libxx.lib 更新了,所有使用它的应用程序都需要重新编译、发布给用户(全量更新)

动态库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新

智能指针

智能指针自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr 允许多个指针指向同一个对象, unique_ptr 则“独占”所指向的对象。标准库还定义了一种名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象,这三种智能指针都定义在 memory 头文件中

shared_ptr

创建智能指针时必须提供额外的信息,指针可以指向的类型

shared_ptr<string> p1;
shared_ptr<list<int>> p2;

当进行拷贝和赋值时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象

auto p = make_shared<int>(42); // make_shared 函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
auto q(p); // p 和 q 指向相同的对象,此对象有两个引用者

当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象,它是通过析构函数完成销毁工作的

weak_ptr

解决 shared_ptr 循环引用的问题:blog.csdn.net/albertsh/ar…

  • weak_ptr 接受 shared_ptr 类型的变量赋值,但是反过来是行不通的,需要使用lock函数
  • 不增加引用计数

unique_ptr

一个 unique_ptr “拥有“他所指向的对象。与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定的对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁

auto_ptr

STL 容器

顺序容器

是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。顺序容器包括:vector、list、deque

关联容器

关联式容器是非线性的树结构,更准确的说是红黑树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。但是关联式容器提供了另一种根据元素特点排序的功能,这样迭代器就能根据元素的特点“顺序地”获取元素。元素是有序的集合,默认在插入的时候按升序排列。关联容器包括:map(按 key 大小存储)、set、multimap、multiset

map 的所有元素都是 pair,同时拥有 key & value

容器类自动申请和释放内存,因此无需 new 和 delete 操作

在 C++ 11 中新出 4 个关联式容器:unordered_map/unordered_set/unordered_multimap/unordered_multiset

unordered_set(hash_map) 的实现:www.jianshu.com/p/56bb01df8…

一些注意点

  • 对象使用 new 和不使用 new 的区别
    blog.csdn.net/cscmaker/ar…

  • 构造函数要写成 public,private 只能被成员函数和友元访问,否则创建对象时,对象无法访问构造函数

  • 编译只对一个文件进行编译,生成 .s 文件(汇编代码);汇编阶段把汇编代码变可执行文件 .o

  • 当使用 extern 关键字修饰变量(未初始化),表示变量声明。当在另一个文件中,为 extern 关键字修饰的变量赋值时,表示变量定义。声明可以拷贝 n 次,但是定义只能定义一次

  • include 双引号表当前目录找,尖括号表示引用标准库头文件,在系统目录

  • 初始化列表(构造函数中进行的是赋值):

    • 更高效,可以少调用一次默认构造函数
    • 哪些必须要用初始化列表初始化:blog.csdn.net/sinat_20265…
    • 初始化列表在构造函数执行前执行,他不能初始化静态成员,静态成员的初始化见上方。
    • 初始化方式不是按照列表的的顺序,而是按照变量声明的顺序
  • C++ 中子类隐藏父类同名函数(只需同名,参数、返回值无关)、变量;Java 中必须参数、返回值相同才能隐藏父类方法
    指的是子类引用指向子类对象(不是动态绑定)

  • 静态成员函数没有 this 指针,因为 this 指向调用该成员函数的那个对象,而静态成员函数属于整个类拥有

  • #ifdef #endif (一般写在头文件里)作用:防止头文件被多次引用,blog.csdn.net/fly_yr/arti…

  • extern "C":extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。void foo(int x, int y) 函数被 C 编译器修饰后在符号库中的名字是 _foo,而 C++ 编译器则会产生像 _foo_int_int 之类的名字。interview.huihut.com/#/?id=exter…

  • 调试宏:blog.csdn.net/u011192270/…

  • explicit:修饰单参数的构造函数时,可以防止隐式转换和复制初始化

    class A
    {
    public:
        explicit A(int a)
        {
            cout << "A(int a)" << endl;
        }
        A(const A& a)
        {
            cout << "A(const A& a)" << endl;
        }
    
    private:
        int _a;
    };
    
    void doA(A a) {}
    
    int main()
    {
        A a1(1);
        // 复制初始化不通过
        A a2 = 1;
        // explicit 修饰构造函数的对象不可以从 int 到 A 的隐式转换
        doA(1);
    }
    
  • assert:断言,是宏,而非函数。用于判断一个表达式,在表达式条件为 false 的时候触发异常

  • sizeof()

    • 对数组,得到整个数组所占空间大小
    • 对指针,得到指针本身所占空间大小
  • 命名空间:可作为附加信息来区分不同库中相同名称的函数、类、变量等。www.runoob.com/cplusplus/c…

  • using:using 声明和指示。interview.huihut.com/#/?id=using

  • :: 范围解析运算符,例子:interview.huihut.com/#/?id=-范围解析…

    1. 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
    2. 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
    3. 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
  • 右值引用(C++ 11 新特性):概括看这,具体解释看这

  • 为何 static / const 成员函数不能为 virtual?

    • static 成员函数没有 this 指针,而 vptr 是通过 this 指针访问的
    • const 修饰符用于表示函数不能修改成员变量的值,该函数必须是含有 this 指针的类成员函数
  • strcpy 和 memcpy:blog.csdn.net/qq_36389327…

  • 如何定义一个只能在堆上(栈上)生成对象的类?interview.huihut.com/#/?id=如何定义一…

  • lambda 表达式是一种匿名函数(内联函数),即没有函数名的函数;该匿名函数是由数学中的 λ 演算而来的。通常情况下,lambda 函数的语法定义为:
    [capture] (parameters) mutable ->return-type {statement}

    blog.csdn.net/lixiaogang_…