news 2026/6/23 19:17:39

【c++面向对象编程】第31篇:智能指针(二):shared_ptr与weak_ptr——循环引用破解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【c++面向对象编程】第31篇:智能指针(二):shared_ptr与weak_ptr——循环引用破解

目录

一、为什么需要 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_ptrmake_shared
内存分配次数2次(对象 + 控制块)1次(对象和控制块合并)
异常安全有隐患(后面讲)✅ 安全
缓存局部性差(对象和控制块分离)好(在同一内存块)
代码简洁度较啰嗦简洁

异常安全问题(经典坑)

cpp

// 危险写法 void f(shared_ptr<Widget>(new Widget(10)), g());

C++ 函数参数的求值顺序不确定。如果顺序是:

  1. new Widget(10)

  2. 调用g()(可能抛异常)

  3. 构造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

如果在类的成员函数中需要返回指向thisshared_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++ 性能优化的关键。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 19:17:58

babel-plugin-jsx 核心原理揭秘:深入理解 JSX 到 VNode 的转换过程

babel-plugin-jsx 核心原理揭秘&#xff1a;深入理解 JSX 到 VNode 的转换过程 【免费下载链接】babel-plugin-jsx JSX for Vue 3 项目地址: https://gitcode.com/gh_mirrors/ba/babel-plugin-jsx 你是否曾好奇在 Vue 3 项目中&#xff0c;那些优雅的 JSX 语法是如何被浏…

作者头像 李华
网站建设 2026/6/23 19:17:57

ops-collections高级特性解析:条件插入、异步操作与回调函数

ops-collections高级特性解析&#xff1a;条件插入、异步操作与回调函数 【免费下载链接】ops-collections ops-collections是基于昇腾硬件的高性能容器模板库&#xff0c;提供运行在NPU上的static_map、dynamic_map、set等容器。利用最新的SIMT并发能力&#xff0c;支持对容器…

作者头像 李华
网站建设 2026/6/23 19:17:57

Keil MDK 5.23 Flash编程错误分析与解决方案

1. 问题现象与背景分析最近在使用Keil MDK 5.23配合ULINKpro调试器进行Flash编程时&#xff0c;遇到了一个令人困扰的错误。具体表现为&#xff1a;在擦除操作完成后&#xff0c;编程阶段突然失败&#xff0c;系统抛出"Internal DLL Error"错误提示&#xff0c;最终导…

作者头像 李华
网站建设 2026/6/23 19:42:29

Windows 11项目开发者指南:自定义脚本编写与执行最佳实践

Windows 11项目开发者指南&#xff1a;自定义脚本编写与执行最佳实践 【免费下载链接】windows11 &#x1f30e; Windows 11 Settings, Tweaks, Scripts 项目地址: https://gitcode.com/GitHub_Trending/wi/windows11 Windows 11项目是一个专注于系统优化和自定义的开源…

作者头像 李华
网站建设 2026/6/23 19:19:09

缓存侧信道攻击对大型语言模型的安全威胁与防御

1. 缓存侧信道攻击与大型语言模型安全概述在当今云计算和人工智能技术蓬勃发展的背景下&#xff0c;大型语言模型(LLM)已成为自然语言处理领域的核心技术。然而&#xff0c;随着这些模型在金融、医疗和客服等敏感领域的广泛应用&#xff0c;其安全性问题日益凸显。其中&#xf…

作者头像 李华