一.源代码到可执行文件的全流程
1.预处理:预处理器(cpp)将C++源文件变为预处理后的文件。
2.编译:预处理后的文件通过编译器变为汇编语言文件。
3.汇编:汇编语言文件经过汇编器输出为目标文件(二进制指令,符号表与重定位表)。
4.链接:目标文件与库文件链接为可执行文件。
- 静态链接:将库的代码直接复制到可执行文件中,运行时不依赖库。
- 动态链接:仅记录库的名称与路径,运行时由操作系统加载库。
二.多态(开闭原则)
1.静态多态
1)概念:编译阶段就确定函数调用的具体实现,无需运行时判断。
2)实现方法:
- 函数重载
- 运算符重载
- 模板
2.动态多态(虚函数)
1)概念:程序运行时,根据对象的实际类型确定函数调用的具体实现。
2)虚函数:virtual关键词,子类可重写(override)
动态多态触发条件(全部满足条件):
- 基类指针指向子类对象(调用的是子类重写的虚函数,但是变量与非虚函数是父类的)
- 通过基类指针调用虚函数(过程:通过对象的虚指针找到虚表,根据虚函数在虚表中的索引,找到实际函数位置,执行该函数)
cpp
class Base {
public:
virtual void show() { cout << "Base::show()" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived::show()" << endl; } // 重写
};
int main() {
Base* ptr = new Derived(); // 基类指针指向子类对象
ptr->show(); // 运行时调用 Derived::show()(动态多态)
delete ptr;
return 0;
}
3)虚表与虚指针:
(1)虚表:属于类,每个包含虚函数的类都生成一张虚函数表,表中存储该类所有虚函数地址。
- 基类虚表存基类虚函数地址
- 子类若重写基类虚函数,则用子类函数地址覆盖虚表中对应位置,未重写则继承基类地址。
(2)虚指针:属于对象,每个包含虚函数的类的对象中,会隐藏虚指针,指向该类的虚表。
- 对象创建时,虚指针会由构造函数完成初始化。
三.继承
1.本质:让一个类复用,扩展与修改另一个类的属性与行为。
2.继承的访问控制
继承只会缩小与保持成员的访问权限。例如:protected继承,那么基类中的public会在派生类中降级为protected权限(通过派生类对象无法访问基类的public成员)。
3.继承的同名成员处理(重定义/隐藏)
只要派生类与基类存在同名成员(变量/函数),派生类就会隐藏基类的同名成员。
注:隐藏后,默认访问派生类同名成员,若要访问基类同名成员,需用 **基类名::**限定。
四.菱形继承(多继承与虚继承)
1)问题描述:顶层基类A > B与C都继承A > D同时继承B与C
- 数据冗余:D保留两份A的成员。
- 二义性:D访问A的成员,无法确定B路径还是C路径。
2)解决方式:将中间派生类的继承方式改为虚继承。
cpp
class 派生类名 : virtual 继承方式 基类名 { };
注:虚基类的构造函数由最终派生类直接调用,中间派生类对虚基类构造的调用被忽略。中间派生类的虚基类表 存的是虚基类相对于该类起始地址的偏移量。如果要调用虚基类成员,会通过任意一条路访问该虚继承的类的虚基类表,获取虚基类的位置。
五.构造函数
1.概念:对象创建时自动调用,用于初始化对象。
2.特性:
- 自动调用
- 无返回值
- 可重载
- 不能被继承
- 不能为虚函数:构造函数执行时,虚指针需要初始化。
3.继承下的构造顺序:先基类 > 再子类(从根到叶)。
六.析构函数
1)概念:析构函数是类的特殊成员函数,用于在对象生命周期结束执行清理工作。
2)继承下的析构顺序:先子类 > 再基类(从叶到根)。
问:为什么析构函数建议声明virtual?
答:例如:基类指针指向子类对象,基类析构需要声明virtual,否则会导致内存泄漏(只会执行基类析构函数,若子类在堆上分配有资源,子类析构函数永远不会执行)。
七.智能指针
作用:确保对象不再使用会自动释放内存。
1)unique_ptr(独占所有权):独占对象所有权,同一时间只有一个unique_ptr指向同一对象。
2)shared_ptr(共享所有权):共享对象所有权,多个shared_ptr指向同一对象,通过引用计数(线程安全)管理生命周期。
3)weak_ptr(弱引用):不拥有对象所有权,仅作为shared_ptr的"观察者",不会增加/减少计数,主要就解决shared_ptr的循环引用问题。
cpp
auto shared = std::make_shared<int>(30);
std::weak_ptr<int> weak = shared; // 引用计数不变
循环引用:两个类互相持有对方的shared_ptr,此时创建两者对象,各自引用计数+1,并且相互引用,引用计数+1,离开作用域后创建的对象引用计数-1,此时二者引用计数为1,永远不会销毁。
注:引用计数管理的是对象的生命周期。
cpp
class Parent {
public:
std::shared_ptr<Child> child;
};
class Child {
public:
std::shared_ptr<Parent> parent;
};
int main() {
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent;
return 0;
}
八.内存布局
1.栈(Stack)
1)用途:存放函数参数,返回地址与局部变量等。
2)分配方式:编译器自动分配与释放。
3)特点:
- 空间小,分配速度快。
- 内存连续,无内存碎片问题。
2.堆(Heap)
1)用途:存放程序运行时动态分配的对象。
2)分配方式:new/malloc分配;delete/free释放。
3)特点:
- 空间大,分配速度慢。
- 内存不连续,易产生碎片。
3.全局与静态存储区
1)用途:存放全局与静态变量。
2)分配方式:程序启动时由操作系统分配,程序结束时释放。
3)特点:
- 生命周期贯穿整个程序运行期间。
- 未初始化变量会自动置为0或nullptr。
4.常量存储区
用途:存放字符串常量,const修饰的全局常量等只读数据。
5.代码区
用途:存放程序的二进制机器指令。
九.New与Malloc
|----------|---------------|-----------------------|
| 区别 | new | malloc |
| 性质 | C++关键字 | C标准库函数 |
| 重载 | 可以 | 不可以 |
| 返回类型 | 对象类型指针(类型安全) | void*(需强制类型转换,类型不安全) |
| 对象生命周期管理 | 分配内存+自动调用构造函数 | 仅分配原始内存,不调用构造函数。 |
十.指针与引用
|--------|-------------------|-----------------|
| 区别 | 指针 | 引用 |
| 定义与初始化 | * 声明,可选是否初始化。 | & 声明,必须初始化。 |
| 空值性 | 可以为nullptr(可能野指针) | 不可为空 |
| 重新赋值 | 可以改变指向 | 一旦绑定对象,只能修改对象的值 |
| 取地址 | 得到的是指针变量自身的内存地址 | 得到的是所绑定对象的地址 |
十一.常量指针与指针常量
作用:约束指针的行为。
1.常量指针
1)概念:指针指向常量,不可以修改所指对象的值,但可以改变指向。
cpp
int a = 10, b = 20;
const int* p = &a;
p = &b; //p 可以改变指向,现在指向 b
2.指针常量
1)概念:指针本身是常量,不能改变指向,可以修改所指对象的值(对象本身不是const)。
cs
int a = 10, b = 20;
int* const p = &a;
*p = 30; //可以通过 p 修改 a 的值(a 不是 const)
十二.static与const
|------|------------------|----------------|
| 区别 | static | const |
| 作用 | 控制生命周期,作用域 | 实现"只读"约束 |
| 修饰对象 | 全局变量/函数,局部变量与类成员 | 普通变量,指针,引用与类成员 |
| 生命周期 | 延长局部变量生命周期到程序结束 | 无影响 |
| 类成员 | 属于类,所有对象共享 | 属于对象,每个对象有独立副本 |
十三.模板
C++实现泛型编程的核心工具
1.函数模板
cpp
template <typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
mySwap(x, y); //3(int)
double d1 = 1.1, d2 = 2.2;
mySwap(d1, d2); //3.3(double)
2.类模板
cpp
template <typename T>
class DynamicArray {
private:
T* data;
int size;
public:
DynamicArray(int s) : size(s) {
data = new T[size];
}
// 重载 [] 运算符
T& operator[](int index) {
return data[index];
}
};
DynamicArray<int> intArr(5);
intArr[0] = 10;
3.模板全特化与偏特化
1)全特化:给模板的所有参数都指定具体的类型,完全替换通用模板的实现。
2)偏特化:只指定部分模板参数 ,或者对模板参数的类型做限制。
十四.Vector与List
|-------|-------------------------|------------------|
| 区别 | Vector | List |
| 底层结构 | 动态数组 | 双向链表 |
| 空间局部性 | 连续存储(相同类型),缓存命中率高 | 非连续存储,缓存命中率低 |
| 性能 | 下标访问O(1);增删尾部O(1)其余O(n) | 遍历访问O(n);增删O(1) |
| 内存管理 | 当前容量=预分配容量,自动扩容(2倍) | 每个节点单独分配内存,无预留空间 |
Vector的扩容机制:
- size(当前容量) == capacity(预分配容量),则触发扩容。
- STL在堆上申请更大的连续内存,通常为原capacity的k倍(若原capacity = 0,则通常初始分配1个元素的空间)
- 使用移动构造元素,直接转移元素。
- 调用旧内存每个元素的析构函数,清理元素。
- 归还旧的内存块给堆,避免内存泄漏。
- 更新内部状态。
十六.map与unordered_map
STL常用关联容器,存储键值对数据。
1.map(红黑树)
1)作用:防止二叉树在极端条件下退化为链表。
2)规则(只是一种标记,与键值/排序无关):
- 节点是红或黑
- 根节点必须为黑
- NIL节点(无孩子节点,不存储键值对,纯工具节点)必须为黑。
- 每个红节点必须有两个黑子节点(从叶子到根不能有连续的红节点)
- 从任一节点到其子节点的所有简单路径,全部包含相同数目的黑节点。

3)排序:每个节点存储键值对,排序依赖键。
注:当前节点左子树所有节点 < 当前节点,右子树所有节点 > 当前节点。
2.unordered_map(哈希表)
拉链法同C#的字典底层解决哈希冲突。