C++
面向对象
特性
- 封装:隐藏内部实现细节,提供对外接口
- 继承:子类继承父类,实现代码复用与扩展
- 多态:同名方法不同实现
- 静态多态(编译时多态):通过函数重载、运算符重载、模板实现
- 动态多态(运行时多态):通过虚函数和指针/引用实现
类
EBO 机制
一个空类 A,它的 sizeof(A) 是多少?
结果为 1,C++ 规定每个对象必须具有唯一的地址,因此即使为空类,也至少分配 1 个字节
如果还有一个空类 B 继承 A 呢?sizeof(B) 是多少?
sizeof(B) = 1,当一个空类作为基类被继承时,编译器会将它的空间优化掉,这就是 EBO 机制
菱形继承(未解决二义性)四中个都为空类,派生类 D 的 sizeof 是多少?
sizeof(D) = 1,派生类有两个 A 子对象,但它们占的空间都被优化掉了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};
int main(void)
{
A objA;
B objB;
C objC;
D objD;
int a = sizeof(objA);
int b = sizeof(objB);
int c = sizeof(objC);
int d = sizeof(objD);
// 输出为 1 1 1 1
cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
return 0;
}菱形继承(解决二义性)四中个都为空类,派生类 D 的 sizeof 是多少?
由于虚表指针的存在,64位系统下 sizeof(D) = 16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
int main(void)
{
A objA;
B objB;
C objC;
D objD;
int a = sizeof(objA);
int b = sizeof(objB);
int c = sizeof(objC);
int d = sizeof(objD);
// 输出为 1 8 8 16
cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
return 0;
}
基础
栈溢出
拷贝
深拷贝:不仅拷贝对象的成员变量,还会申请新的内存空间,并复制指针所指向的内容
两个对象互相独立,互不影响
浅拷贝:拷贝对象的成员变量,若其为指针,只拷贝指针地址,而非指针指向内容
共享同一份资源,容易产生悬垂指针或者 Double Free 这种未定义行为
移动拷贝:转移资源所有权,规避深浅拷贝的问题
拷贝后修改原对象,拷贝对象是否会改变?
深拷贝:不会改变,因为二者独立
浅拷贝:若为值对象,不会改变;若为指针,会跟着改变,因为二者共用一块内存
引用
是否占空间?
- 类成员引用:占 1 个指针大小空间,因为需要持久绑定在某个对象上
- 函数参数引用:占 1 个指针大小空间,
- 普通局部引用:不占,底层的指针由汇编实现
底层实现原理
类成员引用:汇编层面在对象内存中存储一个指针,通过解引用访问被引用对象
函数参数引用:汇编层面会将原对象的地址放到寄存器/栈上,将其传给函数
普通局部引用:编译器直接展开引用为原对象的访问
引用与指针的区别
- 引用声明时必须初始化,指针不用
- 引用不可初始化为空,指针可以
- 引用初始化后不可修改,指针可以
- 引用是原对象的别名,指针是独立的变量
- sizeof:引用的结果与原对象相同,指针为 4 或 8 字节
extern
STL
容器
sort
迭代器
遍历删除元素,需要注意什么?
新特性
智能指针
auto_ptr
C++98 引入,11 弃用
早期的独占指针,但其在拷贝后会交换所有权,导致原指针变为悬垂指针,故被 unique_ptr 替代
scoped_ptr
Boost 库中支持,更严格的 unique_ptr,不支持资源所有权转移、不可移动、也不支持 STL 容器
unique_ptr
通过将拷贝构造和拷贝赋值绑定为 delete 实现独占;通过将对象生命周期与作用域绑定实现 RAII
支持移动构造和移动赋值转移所有权
shared_ptr
通过控制块实现引用计数,当最后一个 shared_ptr 被销毁时,它所管理的资源才会被释放
线程安全问题
引用计数是原子操作,保证线程安全
1
2
3
4std::atomic<int> count;
count.fetch_add(1, std::memory_order_relaxed); // 增加引用计数
count.fetch_sub(1, std::memory_order_acq_rel); // 减少引用计数shared_ptr 和所管理的对象并不保证线程安全,多线程下要加锁进行同步
与 make_shared 的区别
make_shared
在一连续的堆内存中,同时分配对象和控制块。只 new 一次,更节省内存且更高效
对象和控制块绑定在一起,需要一起释放
不支持自定义删除器
shared_ptr
先给对象分配内存,再给控制块分配内存,需要 new 两次
对象和控制块可以分开释放
支持自定义删除器
enable_shared_from_this
用于从对象内部拿到一个 shared_ptr
- 使用 shared_ptr 会开一个新的引用计数,最终对象会被 delete 两次(导致系统崩溃)
- shared_from_this 从内部隐藏的 weak_ptr 提升出一个 shared_ptr,加入当前对象的控制块,保证 引用计数正确
简化代码如下
1 | template<typename T> |
weak_ptr
观察但不拥有资源,用于打破 shared_ptr 的循环引用,必须与 shared_ptr 配套出现
1 |
|
对比裸指针
智能指针 | 裸指针 | |
---|---|---|
内存管理 | 自动释放资源,避免泄漏和重复释放 | 手动管理,容易泄漏或双重释放 |
所有权表达 | 明确表达“拥有”、“共享”或“观察”的语义(如 unique_ptr / shared_ptr / weak_ptr ) |
无所有权语义,容易误用 |
线程安全 | shared_ptr 是线程安全的(引用计数),但会牺牲性能 |
非线程安全,需手动加锁 |
容器兼容性 | unique_ptr / shared_ptr 可与 STL 容器无缝配合 |
可配合使用,但要手动管理释放 |
性能开销 | 引入一定开销:构造、析构、移动、引用计数(特别是 shared_ptr ) |
性能极轻量,无额外开销 |
循环引用风险 | shared_ptr 容易产生循环引用,需配合 weak_ptr |
没有引用计数,不会循环引用 |
内存布局/碎片 | 存在控制块(特别是 shared_ptr ),可能导致额外内存开销或碎片 |
紧凑、内存结构清晰 |
使用场景 | 推荐用于拥有动态资源的类和结构体,RAII 场景 | 推荐用于轻量访问、非拥有资源场景 |
Lambda 与捕获列表
move 语义
将左值强转为右值,调用移动构造函数,转移资源所有权
1 | template<typename T> |
内存管理
内存模型
1 | 高地址 |
内存对齐
规则:结构体整体大小是最大对齐值的倍数,可用#pragam pack(n)
限制最大对齐值为 n
好处
- 提高访问效率:CPU 按字长来读取内存,对齐后 CPU 可以一次性读取完成
- 跨平台支持:保证不同平台和编译器之间二进制兼容
- 硬件限制:某些平台不支持非对齐访问,否则系统会崩溃