C++ 从入门到精通之八
大纲
- C++ 从入门到精通之一、C++ 从入门到精通之二、C++ 从入门到精通之三
- C++ 从入门到精通之四、C++ 从入门到精通之五、C++ 从入门到精通之六
- C++ 从入门到精通之七、C++ 从入门到精通之八、C++ 从入门到精通之九
- C++ 从入门到精通之十、C++ 从入门到精通之十一
C++ 智能指针
直接内存管理
直接内存管理的示例代码
1 |
|
同一块内存不能 delete 多次
- 不能
delete多次的根本原因是:- 设计假设:内存管理器假设每次
delete的都是正在使用中的内存 - 数据结构破坏:第二次
delete会修改已被修改过的元数据,破坏空闲链表(Free List) - 性能取舍:为了性能,不维护复杂的 “是否已释放” 检测机制
- 不可确定性:第一次
delete的内存可能已被重新分配,导致第二次delete释放的是其他对象的内存
- 设计假设:内存管理器假设每次
delete 操作 | 结果 |
|---|---|
delete p; delete p; | ❌ 未定义行为(通常崩溃) |
delete p; p=nullptr; delete p; | ✅ 安全(delete nullptr 无效果) |
两个指针指向同一内存,多次 delete | ❌ 未定义行为 |
delete nullptr 多次 | ✅ 安全 |
- 错误写法一:同一指针的重复删除
1 | int* p = new int(10); |
- 错误写法二:两个指针指向同一地址
1 | int* p1 = new int(10); |
- 错误写法三:拷贝指针后重复删除
1 | void badFunction(int* ptr) { |
- 特殊情况:
delete空指针是合法的
1 | int* p = nullptr; |
delete 操作的最佳实践总结
delete后立即置空
1 | delete p; |
- 使用智能指针(推荐)
1 | unique_ptr<int> p1 = make_unique<int>(42); // 自动管理 |
- 明确指针所有权
1 | // 明确谁拥有指针,谁负责删除 |
最佳实践总结
- 每个
new只对应一个delete,delete后立即将指针置为nullptr,更好的是方案使用智能指针自动管理内存。
new、delete 探秘
重载 new、delete 运算符
1 |
|
程序运行输出的结果如下:
1 | operator new called, size: 4 |
特别注意
- 在 C++ 中,
new和delete具备对堆上分配的内存进行初始化和释放的能力,这是malloc()、free()不具备的。 - 对象构造时,先使用
new分配对象内存,然后再调用对象的构造函数;对象析构时,先调用对象的析构函数,再调用delete释放对象内存。
如何记录 new 分配的内存大小供 delete 使用
C++ 编译器或运行时库在 new 分配内存时,通常会在用户请求的内存前额外多分配一块空间,用于存储内存块的大小信息,然后返回偏移后的指针;delete 根据指针向前访问这个内存块大小信息,从而知道实际分配的内存大小并释放。new 的底层通常调用 malloc() 来分配内存,而 malloc() 自身在内存块前维护了内存块的大小信息,以便 free() 正确释放内存;这些内存块大小信息由底层内存管理器维护,而不是由 new 本身维护。
- 理解注意事项:
- (1) 当调用
malloc(totalSize)时,底层内存管理器(C 标准库)会记录这块内存的实际大小和管理信息(通常在内存块头部隐藏存储)。 - (2) 当调用
free(ptr)时,它会读取自己内部的管理信息来知道这块内存的大小,然后回收,不需要用户手动告诉它大小。 - (3) 因此在
operator delete中,用户只需要传给free()原始分配的指针,底层内存管理器会自动处理。
- (1) 当调用
1 |
|
为什么在上述的 test03() 函数中,如果不手动通过 delete[] 释放对象数组时,会泄露 10 个字节呢?
(1) 对象大小
MyClass类只有构造函数和析构函数,没有成员变量,对象本身大小为 1 字节(空类保证每个对象都有唯一地址)。- 在大多数 64 位编译器下,空类大小通常是 1 字节。
- 如果类里有成员变量,例如
int m_i,类的大小通常是sizeof(int),按 4 或者 8 字节对齐。
(2) 数组长度信息
- 编译器在用
new[]分配数组时,通常会额外在内存块前额外多分配一块空间来存储数组长度信息,用于告诉delete[]需要调用多少次析构函数。 - 这个数组长度通常是
size_t(在 64 位系统上占 8 字节)。
- 编译器在用
(3) 内存块大小信息
new底层调用malloc()分配内存,malloc自身会在内存块前额外多分配一块空间,用于存储内存块的大小信息,以便free()回收内存。- 这内存块大小信息通常占 8 ~ 16 字节(依赖系统和实现)。
(4) 内存布局示意(简化版)
1
2
3
4
5
6
7
8
9+------------------------+
| 内存块大小 = 8 ~ 16 字节 | <-- malloc 内部隐藏头(管理内存块大小)
+------------------------+
| 数组长度 size_t = 8 字节 | <-- 编译器存储数组元素数量
+------------------------+
| 对象 MyClass[0] = 1 字节 |
+------------------------+
| 对象 MyClass[1] = 1 字节 |
+------------------------+(5) 实际泄露的内存大小
- 对象数据本身大小:
2 × sizeof(MyClass) - 额外存储的数组长度信息:
sizeof(size_t)(在 64 位系统上占 8 字节) - 额外存储的内存块大小信息:约占 8 ~ 16 字节(依赖系统和实现)
- 对象数据本身大小:
(6) 举例说明
- 假设是 64 位系统,空类大小占 1 字节,数组长度(
size_t)占 8 字节,内存块大小信息占 8 字节:1
总的内存泄漏大小 ≈ 8 (内存块大小) + 8 (数组长度) + 2 × 1 (对象大小) = 18 字节
- 如果类有一个
int成员变量(4 字节),并按 4 字节对齐:1
2sizeof(MyClass) = 4字节
总的内存泄漏大小 ≈ 8 (内存块大小) + 8 (数组长度) + 2 × 4 (对象大小) = 24 字节
- 假设是 64 位系统,空类大小占 1 字节,数组长度(
为什么在上述的 test02() 函数中,如果不手动通过 delete[] 释放对象数组时,会泄露 16 个字节呢?
(1) 对象大小
int在 64 位系统上通常是占用 4 字节(32 位整数)。- 数组有 2 个元素,所以对象数据占用:
1
2 × sizeof(int) = 2 × 4 = 8 字节
(2) 数组长度信息
- 编译器在用
new[]分配数组时,通常会额外在内存块前额外多分配一块空间来存储数组长度信息,用于告诉delete[]需要调用多少次析构函数。 - 对于
int这种基础类型没有析构函数,数组长度信息通常会被编译器优化掉不存储(也就是说不会占用额外字节)。 - 因此数组长度占用 0 字节。
- 编译器在用
(3) 内存块大小信息
new底层调用malloc()分配内存,malloc自身会在内存块前额外多分配一块空间,用于存储内存块的大小信息,以便free()回收内存。- 这内存块大小信息通常占 8 ~ 16 字节(依赖系统和实现)。
(4) 内存布局示意(简化版)
1
2
3
4
5
6
7+------------------------+
| 内存块大小 = 8 ~ 16 字节 | <-- malloc 内部隐藏头(管理内存块大小)
+------------------------+
| 对象 MyClass[0] = 4 字节 |
+------------------------+
| 对象 MyClass[1] = 4 字节 |
+------------------------+(5) 实际泄露的内存大小
- 对象数据本身大小:
2 × sizeof(int) - 额外存储的数组长度信息:0 字节(基础类型)
- 额外存储的内存块大小信息:约占 8 ~ 16 字节(依赖系统和实现)
1
总的内存泄漏大小 ≈ 8 (内存块大小) + 2 x 4 (对象大小) = 16 字节
- 对象数据本身大小:
关键点
- 数组长度信息:由编译器自己存储,用于告诉
delete[]需要调用多少次析构函数。 - 内存块大小信息:由
malloc()/ 底层内存管理器存储,用于告诉free()实际回收的内存大小。
delete[]需要的额外信息- 当用
new MyClass[2]分配数组内存时,编译器必须知道数组里有多少个对象,以便告诉delete[]需要调用多少次析构函数。 - 所以编译器通常会在数组实际内存的前面额外多分配一块空间,用于存储一个
size_t(或者其它形式)记录数组长度,这不是整个内存块的大小,而是数组长度信息。 - 这块数组长度信息只为
delete[]调用析构函数而服务,与底层内存管理器(malloc)内部记录的 “内存块大小信息” 是两回事。
- 当用
operator delete/free需要的额外信息- 底层内存管理器(
malloc/free)内部会记录每块内存的大小,但这是给free()自己用的,不是编译器额外添加给delete[]使用的。 - 用户不需要知道每块内存的大小,也不需要将内存大小传给
free(),因为free()会自动查到。
- 底层内存管理器(
特别注意
在 C++ 中,使用 new[] 分配数组内存后,必须使用 delete[] 释放内存;若误用 delete 释放数组内存,仅当数组元素类型为基础类型或者不含自定义析构函数的类类型时,行为才不是未定义的(通常不会引发错误),否则会导致内存泄漏或程序崩溃。正确写法应始终成对使用 new[] 与 delete[]。
智能指针的使用
智能指针的类型
在 C++ 中,智能指针的类型有以下几种:
- (1) 带引用计数的智能指针:
shared_ptr - (2) 不带引用计数的智能指针:
auto_ptr、scoped_ptr、unique_ptr - (3) 特殊的智能指针:
weak_ptr(不增加引用计数,可以用于避免shared_ptr发生循环引用)
| 智能指针 | C++ 标准 | 所有权 | 带引用计数 | 适用场景 | 核心特性 |
|---|---|---|---|---|---|
auto_ptr | C++ 98 | 独占(拷贝时转移) | 否 | ⚠ 已废弃,建议改用 unique_ptr | 独占所有权,在复制或赋值时会转移所有权,导致原指针变为空(nullptr) |
scoped_ptr | Boost | 独占 | 否 | 生命周期受限于作用域,适用于简单的场景,避免资源泄漏 | 独占所有权,不可复制或赋值,不支持移动语义,即不可以使用 std::move() 函数转移所有权 |
unique_ptr | C++ 11 | 独占 | 否 | 资源独占,生命周期明确 | 独占所有权,不可复制(拷贝构造和赋值),但可以移动(移动构造和移动赋值),即支持使用 std::move() 函数转移所有权 |
shared_ptr | C++ 11 | 共享 | 是 | 资源共享,生命周期不固定 | 共享所有权(允许多个智能指针管理同一个资源) |
weak_ptr | C++ 11 | 观察 shared_ptr | 否 | 避免 shared_ptr 发生循环引用 | 不增加引用计数,用于避免 shared_ptr 发生循环引用,可以通过 lock() 函数转换为 shared_ptr |
shared_ptr 智能指针
shared_ptr 的使用语法
正确的使用语法
shared_ptr的正确用法- 第一种正确用法
shared_ptr<int> sp(new int(100));shared_ptr<MyClass> sp(new MyClass());
- 第二种正确用法
shared_ptr<int> sp = make_shared<int>(100);shared_ptr<MyClass> sp = make_shared<MyClass>();
- 第三种正确用法
shared_ptr<int> sp;sp是指向int的智能指针,但目前指向的内存地址为空,即属于空指针
- 第一种正确用法
错误的使用语法
shared_ptr的错误用法- 第一种错误用法(代码编译失败)
shared_ptr<int> sp = new int(100);- 智能指针是
explicit,不可以进行隐式类型转换,必须用直接初始化形式
- 第二种错误用法(代码可以正常运行,但不推荐使用)
int *pi = new int(100); shared_ptr<int> spi(pi);- 裸指针与智能指针尽量不要混用,否则会影响代码的健壮性
- 第一种错误用法(代码编译失败)
shared_ptr 的引用计数
特别注意
在 C++ 中,shared_ptr 的引用计数操作是线程安全的(使用原子操作),但 shared_ptr 指向的对象本身不是线程安全的,需要额外同步处理。
引用计数增加
shared_ptr引用计数增加的说明- 移动操作:移动构造 / 移动赋值不会增加原对象的引用计数,只是转移所有权
1
2
3shared_ptr<int> p1 = std::make_shared<int>(10); // 引用计数为 1
shared_ptr<int> p2 = std::move(p1); // 移动构造,p1 变为空指针,p1 指向的原对象的引用计数仍为 1
p1 = std::move(p2); // 移动赋值,同样不增加引用计数
- 移动操作:移动构造 / 移动赋值不会增加原对象的引用计数,只是转移所有权
shared_ptr引用计数增加的情况- (1) 拷贝构造
1
2
3shared_ptr<int> p1 = std::make_shared<int>(42); // 引用计数为 1
shared_ptr<int> p2(p1); // 引用计数为 2
shared_ptr<int> p3 = p1; // 引用计数为 3 - (2) 拷贝赋值
1
2
3
4shared_ptr<int> p1 = std::make_shared<int>(10); // 引用计数为 1
shared_ptr<int> p2 = std::make_shared<int>(20); // 引用计数为 1
p2 = p1; // p2 改为指向 p1 的对象,p2 指向原对象的引用计数从 1 减少到 0,释放资源
// p1 指向的对象引用计数从 1 增加到 2 - (3) 从
weak_ptr构造shared_ptr1
2
3
4
5
6
7shared_ptr<int> sp = std::make_shared<int>(5);
std::weak_ptr<int> wp = sp; // 不影响引用计数
shared_ptr<int> sp2 = wp.lock(); // 成功时返回 shared_ptr,引用计数加 1
if (sp2) {
// sp2 有效,原对象引用计数变为 2
} - (4) 智能指针作为函数的形参(按值传递)
1
2
3
4
5
6
7// 注意:如果这里的形参是引用(按引用传递),那么智能指针的引用计数不会增加
void func(shared_ptr<int> sp) {
}
shared_ptr<int> sp = make_shared<int>(100); // 引用计数为 1
func(sp); // 引用计数为 2 - (5) 智能指针作为函数的返回值
1
2
3
4
5
6
7std::shared_ptr<int> create() {
shared_ptr<int> sp = std::make_shared<int>(50); // 引用计数为 1
return sp; // 返回时可能移动或拷贝,但不一定增加引用计数
}
shared_ptr<int> sp = create(); // 通常会使用移动语义,引用计数保持 1
// 但如果编译器没有优化,可能会临时增加引用计数到 2 - (6) 容器操作(拷贝元素)
1
2
3
4std::vector<std::shared_ptr<int>> vec;
shared_ptr<int> sp = std::make_shared<int>(10); // 引用计数为 1
vec.push_back(sp); // 拷贝到容器,引用计数为 2
vec.insert(vec.begin(), sp); // 再次拷贝到容器,引用计数为 3
- (1) 拷贝构造
引用计数减少
shared_ptr引用计数减少的说明- 当引用计数减少到 0 时,
shared_ptr会:- (1) 调用删除器释放管理的对象
- (2) 释放控制块(如果没有
weak_ptr指向它)
- 移动操作:移动构造 / 移动赋值不会减少原对象的引用计数,只是转移所有权
1
2shared_ptr<int> p1 = std::make_shared<int>(42); // 引用计数为 1
shared_ptr<int> p2 = std::move(p1); // 移动后 p1 变为空指针,但 p1 原来的引用计数不变(转移而非减少) weak_ptr不会影响引用计数,但weak_ptr可以延长控制块的生存时间1
2
3
4shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // 不影响引用计数
sp.reset(); // 引用计数从 1 减到 0,对象销毁
// 但控制块仍存在,直到 wp 也销毁
- 当引用计数减少到 0 时,
shared_ptr引用计数减少的情况- (1) 智能指针对象被销毁
1
2
3
4void func() {
shared_ptr<int> p = std::make_shared<int>(42); // 引用计数 = 1
// ...
} // 离开作用域,p 被销毁,引用计数减为 0,释放资源 - (2) 智能指针指向新的对象
1
2
3shared_ptr<int> sp = make_shared<int>(100); // 引用计数为 1
shared_ptr<int> sp2 = sp; // 引用计数为 2
sp = make_shared<int>(200); // sp 指向新对象,sp 指向的原对象的引用计数减为 1 - (3) 智能指针的
reset()方法被调用1
2
3shared_ptr<int> p = std::make_shared<int>(10); // 引用计数为 1
p.reset(); // 引用计数为 0,释放资源
p.reset(new int(20)); // 先减原引用计数,再管理新资源 - (4) 容器中移除或替换元素
1
2
3
4
5std::vector<std::shared_ptr<int>> vec;
vec.push_back(std::make_shared<int>(1)); // 引用计数为 1
vec.push_back(std::make_shared<int>(2));
vec.erase(vec.begin()); // 第一个元素引用计数减 1
vec[0] = std::make_shared<int>(3); // 第二个元素引用计数减 1
- (1) 智能指针对象被销毁
shared_ptr 的常用操作
use_count()
use_count():返回有多少个智能指针指向某个对象,主要用于代码调试目的
1 | shared_ptr<int> sp = make_shared<int>(100); |
unique()
unique():判断该智能指针是否独占某个指向的对象,也就是如果只有一个智能指针指向某个对象,那么unique()会返回1,否则返回0
1 | shared_ptr<int> sp = make_shared<int>(100); |
reset()
reset():用于改变或释放shared_ptr所管理的对象reset()不带参数:- 唯一指向时:若当前
shared_ptr是唯一指向所管理对象的智能指针(即引用计数为 1),则释放原对象的内存,然后将当前shared_ptr置为空指针 - 非唯一指向时:若有多个
shared_ptr共享同一个对象(引用计数 > 1),则不释放原对象的内存,仅将原对象的引用计数减 1,然后将当前shared_ptr置为空指针 - 无论是哪种情况,调用
reset()后,当前shared_ptr都会变为空指针(nullptr)
- 唯一指向时:若当前
reset(newPtr)带参数:- 唯一指向时:若当前
shared_ptr是唯一指向所管理对象的智能指针(即引用计数为 1),则释放原对象的内存,然后将当前shared_ptr指向新对象(newPtr) - 非唯一指向时:若有多个
shared_ptr共享同一个对象(引用计数 > 1),则不释放原对象的内存,仅将原对象的引用计数减 1,然后将当前shared_ptr指向新对象(newPtr) - 无论是哪种情况,原对象的引用计数都会减少 1(唯一指向时减到 0 则释放原对象的内存,非唯一指向时仅减 1 不释放原对象内存),然后将当前
shared_ptr转而管理新对象(newPtr) - 注意:参数
newPtr是裸指针(通过new得到的原始指针),另外还有两个参数的重载版本reset(newPtr, deleter),用于替换并指定自定义删除器
- 唯一指向时:若当前
- 如果
reset()抛出异常(如自定义删除器在释放内存时抛出异常),原对象保持原样(强异常保证)
(1) 代码案例一
1 | shared_ptr<int> sp = make_shared<int>(100); |
- (2) 代码案例二
1 | auto p1 = std::make_shared<int>(100); // 对象 A |
- (3) 代码案例三
1 | shared_ptr<int> sp; // 空指针 |
* 解引用
- 通过
*获取shared_ptr所指向的对象
1 | shared_ptr<int> sp = make_shared<int>(100); |
get()
get():返回shared_ptr存储的裸指针(通过new得到的原始指针)shared_ptr返回的裸指针,千万不要手动delete,否则会导致程序运行崩溃(未定义行为)- 注意:如果
shared_ptr释放了所指向对象的内存,那么get()返回的裸指针也会变得无效
1 | shared_ptr<int> sp = make_shared<int>(100); |
swap()
swap():用于交换两个shared_ptr所指向的对象
1 | shared_ptr<int> sp = make_shared<int>(100); |
= nullptr
= nullptr- 唯一指向时:若当前
shared_ptr是唯一指向所管理对象的智能指针(即引用计数为 1),则释放原对象的内存,然后将当前shared_ptr置为空指针 - 非唯一指向时:若有多个
shared_ptr共享同一个对象(引用计数 > 1),则不释放原对象的内存,仅将原对象的引用计数减 1,然后将当前shared_ptr置为空指针 - 无论是哪种情况,执行
= nullptr后,当前shared_ptr都会变为空指针(nullptr)
- 唯一指向时:若当前
1 | shared_ptr<int> sp = make_shared<int>(100); |
shared_ptr 的删除器
自定义删除器
shared_ptr会在一定的时机自动删除所指向的对象,并且底层使用delete操作符作为默认的资源析构方式shared_ptr还支持指定自定义的删除器来取代 C++ 编译器提供的默认删除器(1) 代码案例一
1 | void myDeleter(int *p) { |
- (2) 代码案例二
1 | // 自定义的删除器还可以是 Lambda 表达式 |
make_shared 不支持自定义删除器的原因
- 设计目的:
make_shared是为了一次分配内存(控制块和对象内存在一起)和异常安全,而自定义删除器通常用于管理非new分配的资源,比如文件句柄、自定义释放逻辑(动态数组)等 - 类型推导问题:删除器的类型会改变
shared_ptr的类型签名,而make_shared无法推断这种类型信息 - 语义清晰:需要自定义删除器说明资源不是普通方式分配的,此时使用
new显式创建对象更符合意图
使用数组的问题
如果
shared_ptr指向的是数组,那么就需要自定义删除器或者特殊语法,否则 C++ 编译器不会使用delete []正确释放数组的内存,而是默认使用delete导致内存泄漏
1 | // 自定义类(带自定义的析构函数) |
程序运行输出的结果如下:
1 | ~MyClass() |
除了上面通过函数或者 Lambda 表达式作为自定义删除器来释放数组内存,还可以使用
std::default_delete来做删除器,std::default_delete是标准库(STL)里的模板类,同样可以正确释放数组的内存
1 | // 自定义类(带自定义的析构函数) |
程序运行输出的结果如下:
1 | ~MyClass() |
如果实在不想通过自定义删除器或者
std::default_delete来正确释放数组的内存,还可以使用数组特化版本shared_ptr<T[]>,同样可以正确释放数组内存(从 C++ 17 开始支持以下写法)
1 | class MyClass { |
程序运行输出的结果如下:
1 | ~MyClass() |
同样的,还可以封装一个函数模板来正确释放数组内存
1 | class MyClass { |
程序运行输出的结果如下:
1 | ~MyClass() |
同一种类型的说明
特别注意
如果两个 shared_ptr 指定了不同的删除器,只要它们所指向的对象类型是相同的,那么这两个 shared_ptr 也属于同一种类型。这就代表只要类型相同,就可以将它们放入到元素类型为该对象类型的容器里面。
1 | int main() { |
程序运行输出的结果如下:
1 | delete by lambda2 |
shared_ptr 的使用陷阱
裸指针与智能指针混合使用
将一个裸指针绑定到一个 shared_ptr 之后,那内存管理的责任就应该交给 shared_ptr 了,这个时候就不能够再通过裸指针来访问 shared_ptr 所指向的内存。简而言之,在 C++ 中应该尽量避免同时使用裸指针和智能指针(如 shared_ptr)。
- 错误使用案例
1 |
|
- 正确使用案例
1 |
|
使用裸指针初始化多个智能指针
C++ 禁止使用同一个裸指针初始化多个独立的 shared_ptr,核心原因是避免重复释放内存。每个 shared_ptr 都有自己的控制块,如果从同一个裸指针分别创建,它们会互不知情,导致析构时对同一块内存释放两次,造成未定义行为。正确的做法是先创建一个 shared_ptr,然后用它拷贝构造其他 shared_ptr,这样它们才能共享同一个控制块和引用计数。
- 错误使用案例
1 |
|
- 正确使用案例
1 |
|
谨慎使用 get () 返回的裸指针
在 C++ 中,需要谨慎使用 shared_ptr 的 get() 返回的裸指针,是因为这个裸指针不参与引用计数管理。如果你 delete 这个裸指针,会导致原始内存被释放,但 shared_ptr 仍认为它拥有这块内存,最终在析构时会再次尝试释放,造成重复释放内存的未定义行为。此外,如果用这个裸指针去初始化另一个 shared_ptr,也会造成独立的控制块,同样导致重复释放内存。get() 返回的裸指针仅适合用于那些不转移所有权、不延长生命周期的只读访问场景。
- 错误使用案例
1 |
|
- 正确使用案例
1 |
|
不要将 this 作为 shared_ptr 返回
在 C++ 中,将 this(类对象指针)作为 shared_ptr 返回是危险的,因为 this 本质上是一个原始指针,用它直接构造 shared_ptr 会创建一个独立于现有引用计数体系的新控制块。这会导致同一个对象被多个相互不知晓的 shared_ptr 管理,从而在析构时引发重复释放内存(Double Free)的未定义行为。正确的做法是让类继承自 enable_shared_from_this,然后通过 shared_from_this() 返回安全的智能指针。
- 错误使用案例
1 | class MyClass { |
- 正确使用案例
1 | class MyClass : public enable_shared_from_this<MyClass> { |
应避免 shared_ptr 发生循环引用
在 C++ 中,循环引用是指两个或多个 shared_ptr 互相持有对方的引用,导致每个对象的引用计数始终无法降为零,从而永远不会被自动析构,造成内存泄漏。这是因为 shared_ptr 基于引用计数机制工作,只有当计数变为零时才会释放对象,而循环引用使得每个对象至少还被一个其他对象强引用着。解决方法是使用 weak_ptr 来打破循环:将其中一个方向的引用改为弱引用,weak_ptr 不会增加引用计数,因此不影响对象的生命周期。当需要访问弱引用所指向的对象时,可以临时将其提升为 shared_ptr,若对象仍存在则操作成功,否则说明对象已被释放。
- 错误使用案例
1 | class CB; |
- 正确使用案例
1 | class CB; |
shared_ptr 的性能分析
shared_ptr 的指针大小
在 C++ 中,weak_ptr 和 shared_ptr 的大小是一样的,是裸指针(通过 new 得到的指针)大小的两倍;其中包括两个指针(如下图所示),第一个是指向 T 类型对象的指针,第二个是指向控制块的指针。
1 | int *p; |

提示
控制块是由第一个指向某个对象的 shared_ptr 创建的。
shared_ptr 的移动语义
1 | shared_ptr<int> sp = make_shared<int>(100); |
note
对于 shared_ptr,移动构造函数的效率高过拷贝构造函数,移动赋值运算符的效率高过拷贝赋值运算符,因为拷贝需要增加引用计数,而移动语义不需要增加引用计数。
线程安全问题的解决
使用智能指针(如 shared_ptr)后,可以解决在多个线程同时访问同一个对象(共享对象)时产生的线程安全问题,详细介绍可以看 这里。
