运算符优先级
| 优先级 | 类型 | 运算符 |
|---|---|---|
| 0 | 一元后缀运算符 | () [] ++ – -> . |
| 1 | 一元前缀运算符 | ++ – + - ! * & (type) sizeof |
| 2 | 算术运算符 | * / % > ± |
| 3 | 位运算符 | >> << |
| 4 | 关系运算符 | >= > < <= > == != |
| 5 | 位逻辑运算符 | & > ^ > | |
| 6 | 逻辑运算符 | && > || |
| 7 | 三元运算符 | ? : |
| 8 | 赋值运算符 | = += -= *= /= %= … |
| 9 | 逗号运算符 | , |
数组指针&指针数组
函数指针 & 指针函数
1 | /*** 函数指针 ***/ |
数组指针 & 指针数组
1 | int (*a)[10] //一个指针,指向int[10]数组; 对应二级指针级别 int **a、int a[][] |
This
- 指向对象的首地址,只能在非静态成员函数中使用。当类的非静态成员函数访问非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数 this 传递给函数,对各成员的访问均通过 this 进行。this 在成员函数的开始执行前构造,在成员的执行结束后清除。所以This不占用对象空间,会因编译器的不同放置到栈、寄存器、甚至全局变量中
- 用途:除了指向首地址,还能区分形参和成员变量名、返回类对象本身
Define
- 宏主要用于定义常量及书写复杂的内容,不同于typedef定义类型别名;
- 宏在预处理阶段完成替换,属于文本插入替换,比函数执行更快;而const、typedef在编译阶段。
- 宏不检查类型,对应函数没有返回值、没有参数类型;
- 宏不是语句,不在最后加分号。
Static
普通变量
- 可见性本文件,存储在静态存储区(默认初始化为0,全局生存期)
- 全局static变量在
main()之前初始化,局部static在执行到声明处时才初始化
成员变量
- 属于类不占用对象空间,被所有对象共享,在类内定义时分配空间,必须在类定义外初始化
- static成员变量可以被任意成员函数任意访问
成员函数
- 不具有this指针,不能被声明为const、虚函数
- 无法访问对象的非static成员变量/函数
Const
普通变量
- 可见性本文件,存储在栈区(
局部)、常量存储区(全局)或不分配内存在符号表(如基础数据类型const int a = 1)
- 可见性本文件,存储在栈区(
成员变量
- 只在某个对象生命周期是常量,不同对象的const成员值可以不同
- 不能在类定义外部初始化,通过构造函数初始化列表初始化
成员函数
- 不能和static同时使用:static不能实例化和const矛盾
- 常量对象只能调用常量函数
- 想在const成员函数中修改类成员变量,可以用
mutable修饰
与 Constexpr 对比
- const可以通过const_cast类型转换(理解为只读变量),而常量表达式中的成员都是常量,常量表达式一旦确定将无法修改
- const可以修饰编译期和运行期的常量,而constexpr只能修饰编译期的常量,必须在编译期就能计算出来,实现更多的编译期计算,但也会增加编译时间
- 在修饰成员函数时(尾部加const),const只能用于非静态成员函数,因为静态成员函数没有this指针,无法保证不修改对象成员信息;而constexpr可以和成员、非成员、构造函数一起使用,constexpr修饰自定义类时,需要提供常量构造函数
顶层与底层 const
- 顶层const:修饰的变量本身是一个常量,无法修改,指的是指针,顶层const不构成重载
- 底层const:修饰的变量所指向的对象是一个常量,指的是所指变量
Volatile
volatile 用在读取和写入不应被优化掉的内存上。是用来 处理特殊内存 的一个工具。跟多线程无关,不是一种同步手段。
最常见的“特殊”内存是用来做内存映射I/O的内存。这种内存实际上是与外围设备(比如外部传感器或者显示器,打印机,网络端口)通信,而不是读写通常的RAM
volatile 表示变量可以被某些编译器未知的因素(操作系统、硬件等)更改,告知编译器不应对这样的对象进行优化,否则可能打乱变量读写顺序、优化掉对变量的中间操作、两次读之间没修改就直接读上一次的备份。
顺序性:两个包含volatile变量的指令,编译后不可以乱序。但是在执行中还是可能会乱序,需要由内存屏障保证。
易变性:volatile告诉编译器,某个变量是易变的,当编译器遇到这个变量的时候,只能从变量的内存地址中读取这个变量,不可以从寄存器、或者其它任何地方读取。
Union
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承与被继承
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
- 全局匿名联合必须是静态(static)的
Decltype
返回表达式的类型而不计算
- decltype(变量名):可以获得变量精确类型
- decltype(表达式):(包括decltype((变量名))的情况)可以获得表达式引用类型;除非表达式的结果是纯右值,此时结果仍为值类型
Folding
1 | int i = 4; |
Bind函数
std::bind 可以看作一个通用的函数适配器,将可调用对象与其参数一起进行绑定,生成一个新的可调用对象 std::function 保存
- 将可调用对象和其参数绑定成一个仿函数
- 只绑定部分参数,减少可调用对象传入的参数。
1 | void fun(int a, int b, int c, const int &d) { |
访控与继承
public成员对任何访问者可见private成员只对类内可见protected介于public和private之间,它对于类内与派生类可见。这为继承提供了便利,子类可以访问父类的protected成员。但是子类友元不能访问父类protected成员
堆和栈
| 堆 | 栈 | |
|---|---|---|
| 空间大小 | 堆空间不连续,频繁分配产生碎片,但空间大(受限于有效的虚拟内存) | 连续的一小块内存区域(1~8M),没有碎片 |
| 生长方向 | 堆向上,向高地址方向增长 | 栈向下,向低地址方向增长。 |
| 管理方式 | 程序员控制,都是动态分配 | 编译器自动管理,主要静态分配,也可以用alloca函数动态分配(自动释放) |
| 内存管理机制 | 系统有一个记录空闲内存地址的链表,遍历该链表找到第一个空间满足的堆结点,从空闲结点链表中删除该结点,并将该结点空间分配给程序 | 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报栈溢出异常。 |
| 分配效率 | 堆由C/C++函数库提供,分配内存时需要寻找合适大小的内存,并且获取堆的内容需要两次访问(先获取地址,再访问内存),效率低 | 计算机在底层对栈提供支持,栈地址有专门寄存器存放,栈操作有专门指令 |
指针和引用
从编译角度:
- 指针指向⼀块内存,指针的内容是所指向的内存的地址,在编译时将“指针变量名-指针变量的地址”添加到符号表中。所以指针内容可以改变,区分是否 const 。
- 引⽤是⼀块内存的别名,编译时将”引⽤变量名-引⽤对象的地址“添加到符号表中,符号表⼀经完成不能改变,所以引⽤必须⽽且只能在定义时被绑定到⼀块内存上,不能更改也不能为空,不区分 const。
从参数传递角度,引⽤传递和指针传递都是函数栈空间上的⼀个局部变量,但是:
- 指针参数传递本质是值传递,传递的是地址值。被调函数栈中存放的是传入实参变量的副本。形参指针变了,实参指针不会变。
- 传引⽤的实质是传地址,传递的是变量的地址。被调函数栈中存放的是传入实参变量的地址。对形参的任何操作都被处理成间接寻址,即通过别名(栈中存放的地址)访问主调函数中的本体,因此对形参的操作会影响实参变量。
多态
用统一的接口处理不同类型的对象,使程序灵活可扩展、易维护。
- 静态多态:通过函数重载和模板,在编译期决定调用哪个函数
- 动态多态:子类重写父类的虚函数,通过父类指针调用子类成员函数,运行时根据对象的实际类型调用相应的函数
重载、覆盖、隐藏
重载
同一范围内定义了若干的函数名相同,但参数类型、个数、顺序的不同的函数。根据参数列表决定调⽤哪个函数,不关⼼函数的返回类型。
重写/覆盖
派⽣类中重新定义⽗类中除了函数体外完全相同的虚函数。重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派⽣类中重写可以改为 public。
重定义/隐藏
派⽣类重新定义⽗类中同名的⾮虚函数,父类函数被隐藏。参数列表和返回类型都可以不同,只有父类中同名、同参的 virtual 函数(符合重写条件)才不会被派⽣类中的同名函数所隐藏。
声明、定义、初始化
- 声明分为引用型声明与定义型声明。
- 引用型声明:声明外部变量,如
extern int a,不会分配内存 - 定义型声明:声明一个新变量并分配内存,全局变量默认初始化0,局部变量仍然是垃圾值
- 引用型声明:声明外部变量,如
- 定义为变量分配内存空间,等价于定义型声明。
静态绑定、动态绑定
静态绑定,绑定的是在程序中声明的类型,发⽣在编译期间。如非虚函数。
动态绑定,绑定的是所指对象的实际类型,发⽣在运⾏期间,如虚函数。但是,缺省参数值也是静态绑定的,所以不能重新定义继承来的缺省参数,否则可能调用派生类的虚函数时,使用的是基类的缺省参数值。
多重继承
菱形继承

虚继承
虚基类解决了多继承(菱形继承)时命名冲突和冗余数据的问题,使派生类中只保留一份间接基类的成员,需要在继承方式前加上virtual关键字修饰

虚基类部分被放到对象内存的最后面,且B和C的对象中有隐藏的虚基类表指针(vbptr)指向虚基类表(vbtable)
虚基类表中存放了2个偏移量:
- vbptr 相对于虚函数表指针(vfptr)的偏移量(若没有定义虚函数,偏移量为0)
- vbptr 相对于虚基类(A)部分的偏移量
智能指针
智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源
unique_ptr
保证同⼀时间内只有⼀个智能指针可以指向该对象。转移一个unique_ptr将会把所有权从源指针转移给目标指针,源指针被置空。
unique_ptr不支持拷贝构造和赋值操作,但可以配合 move() 支持移动构造和移动赋值。拷贝操作有一个例外,可以从函数中返回unique_ptr。
可以通过reset()或者赋值nullptr释放管理对象
UniquePtr实现
1 | template<typename T> |
shared_ptr
shared_ptr 内部主要包含数据块指针和控制块指针。控制块中包含一个引用计数和其它一些数据。由于这个控制块需要在多个shared_ptr 之间共享,所以它也是存在于 heap 中的。shared_ptr对象本身是线程安全的,也就是说shared_ptr的引用计数增加和减少的操作都是原子的。

采用引用计数器的方法,允许多个智能指针指向同一个对象。引用计数器跟踪有多少个对象共享同一指针,引用计数保存在堆上,每当多/少一个指针指向该对象时,智能指针内部的引用计数±1,当计数为0的时候会自动的释放动态分配的资源。
make_shared 更高效
- 因为 std::make_shared 参数是个
万能引用,防止数据拷贝 - 减少 new 操作次数,本来两个块需要分别调用 new 分配两次内存空间,现在只需要一次。防止中途异常导致的内存泄漏。
- 控制块和数据块分配的空间相邻,cache访存更高效,也减少内存碎片
线程安全性
如果多个执行线程在没有同步的情况下并发访问同一个 std::shared_ptr 对象,并且使用了 shared_ptr 的非常量成员函数,则将发生数据竞争。除非所有此类访问都是通过
std::atomic_load和std::atomic_store这类函数执行的。
- shared_ptr
ptr, new_ptr; - atomic_store(&ptr, new_ptr);
- shared_ptr
ret = atomic_load(&ptr);
- 引用计数是原子,线程安全
- 并发改变指向时不安全,可能引用计数为0提前析构,例如:
- 智能指针P2被线程
A1和A2共享 - 线程
A1执行P1=P2,P1指向P2,但没来得及计数+1 - 线程
A2执行P2=P3,P2指向P3,此时引用计数0将资源释放了,则P1变成了悬空指针
- 智能指针P2被线程
SharedPtr实现
1 | template<typename T> |
weak_ptr
它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,它只引用不计数。weak_ptr 不保证指向的内存一定有效:如果一块内存被 shared_ptr 和 weak_ptr 同时引用,当所有shared_ptr析构了之后,内存就会被释放,通过 expired() 指向的对象是否已被销毁
- 想使用对象但不管理对象,并且在需要时可以返回对象的 shared_ptr 时,使用 weak_ptr
- 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放,解决方法就是其中一方用 weak_ptr 的方式管理对象从而打破环
1 | class Node { |
异常安全问题
1 | some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception()); |
对于以上代码,C++ 没有规定编译器对函数参数的求值次序,所以有可能出现这样的次序:
- 调用new T分配动态内存
- 调用function_that_can_throw_exception()函数(此时抛异常会内存泄露)
- 调用unique_ptr的构造函数
解决:用 make_unique/shared 确保对象 T 的创建和 unique_ptr 一起
类型转换
- static_cast:主要执⾏⾮多态的基本类型互转。基类子类间上⾏转换安全,下⾏转换不安全,得到错误指针;不能转换掉const、volitale属性
- dynamic_cast:主要用于安全的下行转换,支持运行时识别指针、引用
- (下行转换)须用于含虚函数的类,因为运行时转换须要知道类对象的信息,通过虚函数表得到继承关系
- 必须转换类指针、引⽤或 void*类型
- 上行转换同static_cast,下⾏转换安全,当类型不⼀致时返回nullptr
- const_cast:移除const或volatile属性
- reinterpret_cast:从底层对数据进⾏重新解释,危险性高,依赖具体的平台,可移植性差
代码编译链接
1. 预编译
- 处理
#开头的预编译指令,#define、#include、#ifdef - 删除注释,添加行号和文件标识,生成
.i文件
2. 编译
- 经过词法分析、语法分析、语义分析、优化后,生成汇编代码
.s文件
3. 汇编
- 将汇编代码转成机器码,根据汇编指令和机器指令的对照表翻译即可,生成
.o文件
4. 链接
静态链接:把库中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需要其它的库⽂件
空间浪费:如果多个程序对同一个目标文件有依赖,会存在多个副本
更新困难:库函数修改后需重新编译链接
运行速度快
动态链接:把调⽤的函数所在动态链接库和在其中的位置等信息链接进⽬标程序,程序运⾏的时候再从 DLL 中寻找相应函数代码,因此需要 DLL 的⽀持。
共享库:多个程序依赖同一个库时,只需共享一个副本,减小
更新方便:直接替换原来的目标文件即可
即使只需要一两条命令,也要附带庞大的 DLL 的支持才能运行
内存分配
三种 new 操作
1 | // operator new: 标准库函数, 类似malloc只申请一块原始的未命名的内存 |
new / delete 与 malloc / free的区别
- new 是运算符,malloc 是标准库函数
- new 返回具体类型指针 / 抛bad_alloc异常,malloc 返回void类型指针 / NULL
- new 类型安全,malloc 不安全
- new 自动计算要分配的空间大小,malloc 手工计算
- new 调用
operator new的标准库函数分配空间并调用对象的构造函数,delete 先运行析构函数再调用operator delete的标准库函数释放内存。 - new 封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象
malloc和free的原理
malloc() 分配的是虚拟内存。如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。
malloc分配的空间包括:cookie、debugger header、(对象数量)、对象(数组)、padding、cookie
brk()
分配的内存小于 128 KB 时:通过 brk() 系统调用将堆顶指针向高地址移动,申请一块较大的内存块,然后划分成多个小块的内存,用一个空闲内存块的链表维护,每次申请内存时会从链表中分配一个合适大小的内存块。
调用 free 时不会立即还给操作系统,而是将内存块放回到空闲内存块链表中,等待下次复用,可能彼时这个内存块的虚拟/物理地址的映射关系还在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数。
缺点:系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。
mmap()
分配的内存大于 128 KB 时,通过 mmap() 系统调用中私有匿名映射的方式,在文件映射区分配一块内存。释放内存时会把内存归还给操作系统,内存得到真正的释放。
缺点:mmap() 每次都要执行系统调用。另外 mmap() 分配的内存释放时都会归还给操作系统,于是在第一次访问该虚拟地址的时候就会缺页中断。不仅每次都会发生运行态的切换,第一次访问虚拟地址后还会缺页中断,导致 CPU 消耗较大。
内存模型
Linux 将高地址的 1GB 空间分配给内核,用户空间只剩 3GB 包括:
- 栈:由编译器管理分配和回收,存放局部变量和函数参数。效率很高,但是分配的内存容量有限。
- 堆:由
new、malloc分配的内存块,由应用程序去控制释放。空间很大,但可能内存泄漏和空闲碎片。 - 全局/静态存储区 (bss和data):存放静态变量、全局变量和常量(
static const),在编译的时分配初始化为 0,整个运行期间都存在。static 可以控制变量的可见范围,全局变量不行。 - 常量存储区 (rodata):存放的是字面常量和const变量,不允许修改。
- 代码区 (text):存放程序的二进制代码。
内存泄漏
- 堆内存泄漏:没有
free或delete - 系统资源泄漏:如 SOCKET
- 没将基类析构函数定义为虚函数
内存对齐
什么是内存对齐
计算机系统要求数据的首地址值是某个数 k(通常它为4或8)的倍数
内存对齐的原因
- 平台原因:有的硬件平台不支持访问任意地址上的数据,会抛出硬件异常。
- 性能原因:CPU是按块读取的,访问未对齐的内存需要仿存两次,而对齐的内存仅需要访问一次。
内存对齐的规则
- 确定对齐系数:和编译器有关,可以通过预编译命令
#pragma pack(n),n = 1,2,4设置。 - 确定对齐单位:
min(对齐系数,结构体中最长数据类型长度) - 确定数据的偏移量:
min(成员大小,对齐单位)的整数倍,(可选)在成员之间加上填充字节。 - 结构体尾部填充:保证总大小为对齐单位的整数倍