目录
一、为什么需要 shared_ptr?
二、shared_ptr 的基本用法
常用成员函数
三、引用计数原理
四、make_shared 的优势
异常安全问题(经典坑)
五、循环引用问题(核心难点)
六、weak_ptr:破解循环引用
weak_ptr 的常用操作
七、完整例子:双向链表 + 循环引用
八、shared_ptr 的性能开销
九、this 指针的陷阱:enable_shared_from_this
十、常见错误
1. 用同一个裸指针创建多个 shared_ptr
2. 循环引用不处理
3. 依赖 use_count() 做业务逻辑
4. weak_ptr 不检查直接使用
十一、这一篇的收获
一、为什么需要 shared_ptr?
多个对象需要共享同一个资源:
cpp
class Node { public: vector<Node*> children; Node* parent; // 裸指针:谁来释放?容易悬空 };使用unique_ptr也不行:一个资源只能被一个人拥有,但这里的 parent 和 child 需要共同持有对方(或有向关系)。
shared_ptr允许多个指针共享所有权:内部维护一个引用计数,每当一个新的shared_ptr指向同一资源时计数+1,销毁时计数-1,计数归零时自动释放资源。
cpp
class Node { public: vector<shared_ptr<Node>> children; shared_ptr<Node> parent; // 共享所有权 };二、shared_ptr 的基本用法
cpp
#include <memory> #include <iostream> using namespace std; int main() { // 方式1:直接构造 shared_ptr<int> sp1(new int(42)); // 方式2:make_shared(推荐,后面讲原因) auto sp2 = make_shared<int>(100); // 拷贝:引用计数增加 shared_ptr<int> sp3 = sp1; // 现在 sp1 和 sp3 指向同一内存 cout << "use_count: " << sp1.use_count() << endl; // 2 // 赋值 sp3 = sp2; // sp3 放弃对原资源的引用,原资源计数减1 // sp1 的计数变为 1,sp2/sp3 的计数为 2 return 0; }常用成员函数
| 函数 | 作用 |
|---|---|
use_count() | 返回当前引用计数(调试用,不要依赖业务逻辑) |
get() | 返回裸指针 |
reset() | 释放所有权(计数减1,若归零则释放资源) |
unique() | 判断是否独占(use_count() == 1) |
operator bool() | 判断是否非空 |
三、引用计数原理
cpp
// 内部简化示意(非真实实现) template<typename T> class simple_shared_ptr { T* ptr; int* ref_count; // 共享的引用计数(在堆上) public: simple_shared_ptr(T* p) : ptr(p), ref_count(new int(1)) {} simple_shared_ptr(const simple_shared_ptr& other) : ptr(other.ptr), ref_count(other.ref_count) { (*ref_count)++; // 拷贝时计数+1 } ~simple_shared_ptr() { if (--(*ref_count) == 0) { delete ptr; delete ref_count; } } };关键点:
引用计数在堆上分配,所有
shared_ptr共享这个计数器拷贝/赋值操作增加计数,析构时减少计数
计数归零时才释放资源
四、make_shared 的优势
cpp
// 方式A:直接 new(不推荐) shared_ptr<Widget> sp(new Widget(10)); // 方式B:make_shared(推荐) auto sp = make_shared<Widget>(10);
| 对比 | new+shared_ptr | make_shared |
|---|---|---|
| 内存分配次数 | 2次(对象 + 控制块) | 1次(对象和控制块合并) |
| 异常安全 | 有隐患(后面讲) | ✅ 安全 |
| 缓存局部性 | 差(对象和控制块分离) | 好(在同一内存块) |
| 代码简洁度 | 较啰嗦 | 简洁 |
异常安全问题(经典坑)
cpp
// 危险写法 void f(shared_ptr<Widget>(new Widget(10)), g());
C++ 函数参数的求值顺序不确定。如果顺序是:
new Widget(10)调用
g()(可能抛异常)构造
shared_ptr
如果g()抛异常,new出的内存就泄漏了。
make_shared避免了这个问题,因为它原子地完成分配和构造。
五、循环引用问题(核心难点)
cpp
#include <iostream> #include <memory> using namespace std; class Node { public: shared_ptr<Node> next; ~Node() { cout << "Node 析构" << endl; } }; int main() { auto node1 = make_shared<Node>(); auto node2 = make_shared<Node>(); node1->next = node2; node2->next = node1; // 互相指向!形成环 // 函数结束,node1 和 node2 的引用计数是多少? // node1: 被 node2->next 持有 → 计数至少 2 // node2: 被 node1->next 持有 → 计数至少 2 // 离开作用域后,计数减为 1,永远不会到 0 → 内存泄漏! return 0; } // 没有输出 "Node 析构",内存泄漏了为什么会泄漏:
text
初始: node1(ref=1), node2(ref=1) 赋值后: node1.next 指向 node2 → node2(ref=2) node2.next 指向 node1 → node1(ref=2) 离开作用域: node1 销毁 → node1(ref=1) node2 销毁 → node2(ref=1) 此时 node1 和 node2 互相持有对方的引用计数,永远不会归零
六、weak_ptr:破解循环引用
weak_ptr是一种“弱引用”智能指针:
指向
shared_ptr管理的资源不增加引用计数
可以随时检查资源是否还存在
需要访问资源时,通过
lock()临时升级为shared_ptr
cpp
class Node { public: weak_ptr<Node> next; // 用 weak_ptr 打破循环 // 或者用 shared_ptr 表示"拥有",用 weak_ptr 表示"观察" }; int main() { auto node1 = make_shared<Node>(); auto node2 = make_shared<Node>(); node1->next = node2; // node1 弱引用 node2 node2->next = node1; // node2 弱引用 node1 // 没有循环引用!资源正常释放 return 0; }weak_ptr 的常用操作
cpp
weak_ptr<Node> wp = node1; // 1. 检查资源是否还存在 if (wp.expired()) { cout << "资源已释放" << endl; } // 2. 获取 shared_ptr(需要访问时) if (auto sp = wp.lock()) { // lock() 返回 shared_ptr,如果资源还在 sp->doSomething(); } else { cout << "资源已不存在" << endl; } // 3. 获取引用计数(观察用) cout << "use_count: " << wp.use_count() << endl;七、完整例子:双向链表 + 循环引用
cpp
#include <iostream> #include <memory> #include <vector> using namespace std; class ListNode { public: int value; shared_ptr<ListNode> next; // 拥有下一个节点 weak_ptr<ListNode> prev; // 弱引用上一个节点(打破循环) ListNode(int v) : value(v) { cout << "ListNode(" << value << ") 构造" << endl; } ~ListNode() { cout << "ListNode(" << value << ") 析构" << endl; } void print() const { cout << "Node(" << value << "), prev="; if (auto p = prev.lock()) cout << p->value; else cout << "null"; cout << ", next="; if (next) cout << next->value; else cout << "null"; cout << endl; } }; int main() { cout << "=== 创建双向链表 ===" << endl; auto head = make_shared<ListNode>(1); auto node2 = make_shared<ListNode>(2); auto node3 = make_shared<ListNode>(3); // 建立双向连接 head->next = node2; node2->prev = head; node2->next = node3; node3->prev = node2; // 打印 head->print(); node2->print(); node3->print(); cout << "\n=== 引用计数观察 ===" << endl; cout << "head use_count: " << head.use_count() << endl; cout << "node2 use_count: " << node2.use_count() << endl; cout << "node3 use_count: " << node3.use_count() << endl; // 注意:prev 是 weak_ptr,不影响计数 cout << "\n=== 离开作用域,自动释放 ===" << endl; return 0; }输出:
text
=== 创建双向链表 === ListNode(1) 构造 ListNode(2) 构造 ListNode(3) 构造 Node(1), prev=null, next=2 Node(2), prev=1, next=3 Node(3), prev=2, next=null === 引用计数观察 === head use_count: 1 node2 use_count: 2 ← 被 head->next 持有 node3 use_count: 1 ← 只被 node2->next 持有(node3->prev 不增加计数) === 离开作用域,自动释放 === ListNode(3) 析构 ListNode(2) 析构 ListNode(1) 析构
所有节点正常析构,无泄漏。
八、shared_ptr 的性能开销
| 开销类型 | 说明 |
|---|---|
| 内存 | 控制块(引用计数、弱计数、删除器等),通常 2-3 个机器字 |
| 时间 | 构造/析构/拷贝都需要原子操作(线程安全),比unique_ptr慢 |
| 间接性 | shared_ptr本身是 16 字节(64位系统),访问资源需两次解引用 |
什么时候用shared_ptr:
多个对象需要共享同一资源
资源生命周期不确定,由最后使用者释放
避免过早优化,优先用
unique_ptr
什么时候用unique_ptr:
独占所有权(大多数情况)
性能敏感场景
可转换为
shared_ptr(反之不行)
九、this 指针的陷阱:enable_shared_from_this
如果在类的成员函数中需要返回指向this的shared_ptr,直接shared_ptr<T>(this)会创建独立的控制块,导致重复释放。
cpp
// ❌ 错误做法 class Bad { public: shared_ptr<Bad> getShared() { return shared_ptr<Bad>(this); // 危险!会创建第二个控制块 } };✅ 正确做法:继承enable_shared_from_this
cpp
class Good : public enable_shared_from_this<Good> { public: shared_ptr<Good> getShared() { return shared_from_this(); // 返回已有的 shared_ptr } }; auto p1 = make_shared<Good>(); auto p2 = p1->getShared(); // p1 和 p2 共享同一控制块十、常见错误
1. 用同一个裸指针创建多个 shared_ptr
cpp
int* raw = new int(42); shared_ptr<int> sp1(raw); shared_ptr<int> sp2(raw); // ❌ 两个独立控制块,重复释放
2. 循环引用不处理
cpp
class A { shared_ptr<B> b; }; class B { shared_ptr<A> a; }; // 互相引用 → 内存泄漏3. 依赖 use_count() 做业务逻辑
cpp
if (sp.use_count() == 1) { // 不可靠!可能被其他线程改变 // ... }4. weak_ptr 不检查直接使用
cpp
weak_ptr<int> wp; // ... cout << *wp.lock(); // 可能崩溃,应该先检查
十一、这一篇的收获
你现在应该理解:
shared_ptr:共享所有权,引用计数,最后一个所有者释放资源make_shared:单次内存分配、异常安全,优于new+shared_ptr循环引用:两个
shared_ptr互相指向导致计数永不归零 → 内存泄漏weak_ptr:不增加引用计数,打破循环,通过lock()临时升级使用enable_shared_from_this:正确从成员函数返回shared_ptr<this>
💡 小作业:实现一个有向图的邻接表表示,顶点用
shared_ptr<Vertex>,边用weak_ptr<Vertex>(防止循环)。写一个函数检测两个顶点之间是否有路径,确保不会因为循环引用导致内存泄漏。
下一篇预告:第32篇《移动语义与右值引用:现代C++性能优化核心》——左值右值的区别、移动构造函数、std::move的本质、完美转发初探。这是理解现代 C++ 性能优化的关键。