引言
上一篇我们学习了unique_ptr——独占所有权的智能指针。它解决了"忘记 delete"的问题,但它的独占特性意味着一个对象只能有一个 owner。
现实中有大量场景需要共享所有权:多个窗口共享同一个数据模型、多个线程共享同一个资源、缓存中的对象被多处引用。这时就需要shared_ptr。
shared_ptr通过引用计数来管理共享所有权:每多一个shared_ptr指向同一个对象,计数 +1;每销毁一个,计数 -1。计数归零时,对象自动释放。
第一部分:shared_ptr 的核心原理
一、引用计数
#include <memory> #include <iostream> using namespace std; int main() { shared_ptr<int> p1 = make_shared<int>(42); cout << "引用计数:" << p1.use_count() << endl; // 1 { shared_ptr<int> p2 = p1; // p2 和 p1 共享同一个对象 cout << "引用计数:" << p1.use_count() << endl; // 2 cout << "引用计数:" << p2.use_count() << endl; // 2(相同) shared_ptr<int> p3 = p1; cout << "引用计数:" << p1.use_count() << endl; // 3 } // p3 和 p2 离开作用域,引用计数 -2 cout << "引用计数:" << p1.use_count() << endl; // 1 } // p1 离开作用域,引用计数归零 → 自动 delete二、控制块
shared_ptr不只是把裸指针包起来,它背后有一个控制块。
三、创建方式
// 方式1:make_shared(最推荐!) auto p1 = make_shared<int>(42); // 方式2:用 new(不推荐) shared_ptr<int> p2(new int(42)); // 方式3:从 unique_ptr 移动 unique_ptr<int> up = make_unique<int>(42); shared_ptr<int> p3 = std::move(up); // up 变成 nullptr为什么优先用make_shared?
| 创建方式 | 内存分配次数 | 内存布局 |
|---|---|---|
make_shared | 1 次 | 对象和控制块在同一块内存中 |
new + shared_ptr | 2 次 | 对象一块、控制块一块 |
第二部分:shared_ptr 的常用操作
一、基本操作
auto p1 = make_shared<int>(42); // 访问 cout << *p1 << endl; // 解引用 *p1 = 100; // 修改 // 获取原始指针 int* raw = p1.get(); // 不增加引用计数,不转移所有权 // 判断是否为空 if (p1) { /* p1 不为空 */ } if (p1 != nullptr) { /* 等价 */ } // 引用计数 cout << p1.use_count() << endl; // 是否是唯一持有者 if (p1.unique()) { /* 引用计数 == 1 */ } // 释放 p1.reset(); // 引用计数 -1,归零则释放对象 p1.reset(new int(200)); // 释放旧对象,接管新对象二、作为函数参数
// ✅ 值传递:增加引用计数 void func(shared_ptr<int> p) { cout << p.use_count() << endl; // 2(调用方的 1 + 这里的 1) } // ✅ const 引用:不增加引用计数,只读 void func2(const shared_ptr<int>& p) { cout << *p << endl; } // ✅ 如果只需要对象本身,传引用就行 void func3(int& value) { value++; } int main() { auto p = make_shared<int>(42); func(p); // 引用计数 +1 然后 -1 func2(p); // 引用计数不变 func3(*p); // 直接操作对象,不涉及 shared_ptr }第三部分:shared_ptr 的线程安全性
#include <mutex> #include <thread> auto p = make_shared<int>(0); mutex mtx; // ✅ shared_ptr 本身可以被多个线程安全拷贝 thread t1([p] { // 引用计数 +1 lock_guard<mutex> lock(mtx); (*p)++; // 保护对象操作 }); thread t2([p] { // 引用计数 +1 lock_guard<mutex> lock(mtx); (*p)++; }); // ✅ 引用计数正确 = 3(主线程1 + t1 + t2)第四部分:自定义删除器
// 管理 FILE* auto fileDeleter = [](FILE* f) { if (f) fclose(f); }; shared_ptr<FILE> filePtr(fopen("test.txt", "w"), fileDeleter); fprintf(filePtr.get(), "hello\n"); // 离开作用域 → fclose 自动调用和 unique_ptr 的区别:shared_ptr的自定义删除器存储在控制块中,不同类型删除器的shared_ptr可以互相赋值。
shared_ptr<FILE> p1(fopen("a.txt", "r"), fclose); shared_ptr<FILE> p2 = p1; // ✅ 删除器会被正确共享第五部分:enable_shared_from_this
一、问题场景
class Widget { public: shared_ptr<Widget> getSharedPtr() { // ❌ 错误!每次都创建新的控制块,引用计数独立! return shared_ptr<Widget>(this); } }; auto p1 = make_shared<Widget>(); auto p2 = p1->getSharedPtr(); // p1 和 p2 各有独立的引用计数! // p1 释放 → 对象被 delete // p2 释放 → 再次 delete → 崩溃!二、解决方案
class Widget : public enable_shared_from_this<Widget> { public: shared_ptr<Widget> getSharedPtr() { return shared_from_this(); // ✅ 正确,复用已有的控制块 } }; auto p1 = make_shared<Widget>(); auto p2 = p1->getSharedPtr(); // p1 和 p2 共享同一个控制块 cout << p1.use_count() << endl; // 2必须已经有shared_ptr管理该对象,否则shared_from_this()会抛异常。
第六部分:常见错误
// ❌ 错误1:把同一个裸指针给多个 shared_ptr int* raw = new int(42); shared_ptr<int> p1(raw); // 引用计数 = 1 shared_ptr<int> p2(raw); // 新的引用计数 = 1,不是 2! // p1 释放 → delete raw // p2 释放 → 再次 delete raw → 崩溃! // ✅ 正确:用拷贝 auto p1 = make_shared<int>(42); auto p2 = p1; // 引用计数 = 2 // ❌ 错误2:在不需要的时候传值 void func(shared_ptr<int> p) { ... } // 每次调用引用计数 +1/-1 // ✅ 传引用(不改变所有权时) void func(const shared_ptr<int>& p) { ... } // ✅ 传裸引用(只需要对象本身时) void func(int& v) { ... }总结
一、核心要点
| 特性 | 说明 |
|---|---|
| 所有权 | 共享,多个 shared_ptr 指向同一对象 |
| 创建 | make_shared<T>(args) |
| 引用计数 | use_count(),计数归零自动释放 |
| 线程安全 | 引用计数安全,对象操作需要额外加锁 |
| 自定义删除器 | 存在控制块中,可以共享 |
enable_shared_from_this | 从this安全创建 shared_ptr |
| 头文件 | <memory> |
二、unique_ptr vs shared_ptr
| 对比 | unique_ptr | shared_ptr |
|---|---|---|
| 所有权 | 独占 | 共享 |
| 大小 | 8 字节(无删除器) | 16 字节 |
| 性能 | 原始指针级别 | 引用计数有开销 |
| 创建 | make_unique | make_shared |
| 拷贝 | ❌ | ✅ |
| 使用场景 | 明确单一 owner | 多个 owner |
三、一句话记忆
shared_ptr用引用计数实现共享所有权,计数归零自动释放。make_shared一次分配省内存,引用计数本身线程安全但对象操作需加锁。从this创建shared_ptr需继承enable_shared_from_this。