前言
老C/C++开发,用惯了裸指针的方式,之前发布过一篇文章,专门分享结构体通过函数指针成员的方式是实现类成员函数的效果,本篇风格也一样,从C/C++的裸指针,再延伸到C++智能指针。
C/C++ 聊聊结构体、指针、类_qt 结构体指针-CSDN博客文章浏览阅读759次,点赞8次,收藏21次。本文面向QT开发者,系统梳理C/C++中结构体、指针及类的核心概念与演进关系。重点涵盖:C结构体作为数据聚合器、C++结构体作为轻量级类;指针函数与函数指针的区别及在动态库调用中的应用;类从结构体的延伸,封装、继承、构造/析构顺序、虚函数表与this指针机制;运行时多态(虚函数)与编译时多态(重载、模板)的实现原理。内容紧扣QT开发中对接第三方库和OOP实践需求。https://blog.csdn.net/liangyuna8787/article/details/155556760?spm=1001.2014.3001.5502
概述
裸指针:只保存地址,不表达所有权,也不保证释放。在动态内存场景下,如果不主动释放内存,或者改变保存的地址,都会导致内存泄漏。
智能指针:通过 RAII(资源获取即初始化)机制,①在智能指针对象创建(构造函数调用)时,接管动态分配的内存或其他资源;②当智能指针对象离开作用域(例如函数结束、代码块退出或发生异常栈展开)时,编译器会自动调用其析构函数,从而自动释放所管理的资源。
纯理论会感觉比较生硬,以下从语法编译&&系统&&资源方向拆解出易懂的概念。
语法编译&&系统&&资源
我的一位大学计算机教授老师(杨耀)在给我们上代码怎么变成可执行程序的课程时,详细的讲解了gcc/g++编译器的执行过程,其中最浅显的理解如下:
LIFO 嵌套结构规则
代码中的'{'和'}'之间是一个作用域,他在语法和资源分配上都符合LIFO 嵌套结构(典型数据结构栈--“后进先出”)的规则。(需要注意的是这个LIFO嵌套结构不是系统中的物理栈)
以下是一个常见的代码案例,展示入栈/出栈节点。
//-------------------------------案例1-------------------------------------- int g; // 栈底(第一个入栈):全局作用域 void f() { // 压入:函数体作用域 int a; { // 压入:内层块作用域 int b; // 符号查找:沿栈向上搜索 b → a → g(最近声明优先,即"遮蔽") } // 弹出:b 离开可见性,符号表恢复 } // 弹出:a 离开可见性 //-------------------------------案例2-------------------------------------- void c() { /* ... */ } void b() { c(); } void a() { b(); } int main() { a(); } // 调用序列压栈:main → a → b → c // 返回序列弹栈:c → b → a → main (严格后进先出)注意,'{'和'}'不是维持绝对的作用域资源释放规则,C++语法中也有特殊规则,比如在局部作用域中用static 定义的变量,这个静态变量存储在静态存储区 (.data/.bss),全局变量也存储在静态存储区中,所以在main()函数结束时,才会释放这个特殊的资源。
void f() { static int counter = 0; // 存储在静态数据区 (.data/.bss)--与全局变量一个区域 counter++; } // counter 活到程序结束,下次调用值还在生成/释放资源
LIFO 模型展示了主要生命周期,生命周期中最重要的就是资源的管理(生成/释放),应用程序运行时,内存中主要提供了栈、堆、静态存储区的资源分配。
内存的知识点可以查看我的另外一篇博文,本篇不展开
【QT入门到晋级】内存_qt new 结构体-CSDN博客文章浏览阅读803次,点赞15次,收藏16次。本文面向QT后台开发者,系统讲解C/C++在Linux环境下的内存布局与应用实践,涵盖栈、静态存储区、堆及虚拟地址空间(mmap)四大内存区域的特性、适用场景与性能对比。重点分析各类内存的生命周期、大小限制、效率差异及典型使用案例,如大数组处理、批量数据库操作、高清视频共享内存设计等,为QT开发者提供内存选型与优化依据。https://blog.csdn.net/liangyuna8787/article/details/150637450?spm=1001.2014.3001.5501在语法层次上,LIFO 模型管理的是“变量”,如果其中一个变量是裸指针,当裸指针所在作用域结束时,裸指针也会被直接释放掉,如果在裸指针被释放之前,开发人员通过new申请了堆资源(动态内存),但没有用delete释放堆资源(动态内存),那么这个堆资源就会一直存在,这就是内存泄漏常见的场景。
RAII优化 LIFO 模型
LIFO 模型抽象本身只保证“自动变量的存储随作用域失效”,它并不天然管理堆内存、文件句柄这些非栈资源,这也是在使用裸指针申请堆内存时,必须显示释放堆内存,才能规避内存泄漏的风险(出现异常退出时,不能完全规避这种风险)。
但如果引入RAII机制,以类替代裸指针入栈,释放堆内存交由类的析构函数来完成。这样的好处就是①不需要显示释放堆内存;②异常退出情况下,也不会出现内存泄漏的风险(出栈时,必定会调用这个类的析构函数释放资源)。
以上描述“以类替代裸指针入栈”的特征就是智能指针的特性。智能指针本身就是一个类,按LIFO 模型资源出栈(作用域结束 / 异常展开 / 函数返回)时,必定会通过智能指针的析构函数释放堆内容,以下是流程图:
栈上对象(unique_ptr<T>,8字节) │ 出栈(作用域结束 / 异常展开 / 函数返回) ▼ 析构函数 ~unique_ptr() │ ▼ delete ptr_ ──────► 堆上的 T 对象被销毁智能指针的本质
一个单成员(裸指针)的类模板,把“堆内存的释放”这一行代码搬进了析构函数,从而把一个不受作用域管辖的资源,绑到一个严格受 LIFO 管辖的对象上。unique_ptr是它的最简形式(零开销,delete 直挂),shared_ptr是它的多路复用形式(释放动作变成“原子计数归零判定”,且自己管理着第二层资源——控制块)。
以下是GCC libstdc++(bits/unique_ptr.h)的真实代码
template<typename _Tp, typename _Dp = default_delete<_Tp> > class unique_ptr { // 数据成员不是裸的 pair,而是一个 tuple —— 暗藏 EBO 优化,见下文 tuple<pointer, deleter_type> _M_t; public: /// Destructor, the only important part ~unique_ptr() noexcept { auto& __ptr = _M_t._M_ptr(); if (__ptr != nullptr) get_deleter()(std::move(__ptr)); // 通过删除器释放,而非直接 delete __ptr = pointer(); // 置空 } // reset 的逻辑与析构共享同一实现(do_reset / 直接内联) pointer reset(pointer __p = pointer()) noexcept { pointer __old = get(); _M_t._M_ptr() = __p; if (__old != nullptr) get_deleter()(std::move(__old)); return __old; } // ... };从上面的源码中,可以看到有一个独享智能指针unique_ptr的模板类,这个模板类包含两个参数:①要创建的对象;②自定义的删除函数。
智能指针删除器
默认删除器
不提供第二个参数(自定义的删除函数)时,就调用默认的析构函数,下个给出裸指针和同等效果的智能指针的对比代码
#include <iostream> #include <vector> #include <windows.h> #include <memory> using namespace std; // 自定义测试类,打印所有构造/析构的调用信息 class MyClass { private: int val; public: // 普通构造函数 MyClass(int v = 0) : val(v) { cout << "[调用构造函数] val = " << val << endl; } void printMessage(const char* msg){ std::cout << "Message: " << msg << std::endl; } // 析构函数 ~MyClass() { cout << "[调用析构函数] val = " << val << endl; } }; void old_process(int v = 0) { MyClass* obj = new MyClass(v); // 手动分配 obj->printMessage("hello nomal"); delete obj; // 必须手动释放 } void uq_process(int v = 1) { // 用 make_unique 创建,异常安全,避免 new 裸调用 std::unique_ptr obj = std::make_unique<MyClass>(v); obj->printMessage("hello unique"); // 离开作用域时,unique_ptr 自动析构 MyClass } int main() { // 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲 SetConsoleOutputCP(CP_UTF8); ios::sync_with_stdio(true); cout << "=== main函数正式开始执行 ===" << endl; old_process(); uq_process(); return 0; } /*******打印信息******** === main函数正式开始执行 === [调用构造函数] val = 0 Message: hello nomal [调用析构函数] val = 0 [调用构造函数] val = 1 Message: hello unique [调用析构函数] val = 1 ***************/自定义删除器
如果释放资源不仅仅是delete释放堆,还需要进行业务逻辑操作时(比如某个业务类退出时,必须同步通知关联的类时),可以自定义删除器,代码如下
#include <iostream> #include <vector> #include <windows.h> #include <memory> class MyClass { public: MyClass(); ~MyClass(); void printMessage(const char* msg) { // ① 原代码多了一个 {,括号不配对 std::cout << "Message: " << msg << std::endl; } }; #ifdef __cplusplus extern "C" { #endif MyClass* createMyClass() { return new MyClass(); } void destroyMyClass(MyClass* obj) { //-----这里进行其他业务操作 delete obj; } #ifdef __cplusplus } #endif#include <memory> #include "MyClass.h" // 删除器类型直接写函数指针类型: //第二个参数void(*)(MyClass*)是void destroyMyClass(MyClass* obj) 的函数指针 using MyClassPtr = std::unique_ptr<MyClass, void(*)(MyClass*)>; void demo1() { // 构造时必须同时传【对象指针】和【删除器函数】 MyClassPtr p(createMyClass(), &destroyMyClass); //std::unique_ptr<MyClass, void(*)(MyClass*)> p( // createMyClass(), // &destroyMyClass // 自定义删除器,函数指针方式 //); p->printMessage("hello"); } // 析构 → get_deleter()(ptr) → 通过函数指针调用 destroyMyClass(ptr) → delete obj常用智能指针类型
| 类型 | 适用场景 | 注意事项 |
|---|---|---|
unique_ptr | 独占资源、成员变量、工厂返回 | 不能拷贝,只能move |
shared_ptr | 多所有者共享 | 有控制块和原子计数开销 |
weak_ptr | 观察、缓存、打破循环引用 | 使用前必须lock() |
用法流程图
对象死亡时间由谁决定? │ ├─ 当前作用域(函数/类自己管)──────────► unique_ptr ← 默认 │ ├─ 最后一个使用者(多方共享)───────────► shared_ptr │ │ │ ├─ 共享者是【回调/线程】?──► 按值捕获进 lambda(场景3/4) │ ├─ 共享者是【多个容器】?──► 容器存 shared_ptr(场景2) │ ├─ 共享者想【只读快照】?──► shared_ptr<const T> / COW(场景1/7) │ ├─ 有【互指/回指】?─────► 从属方改 weak_ptr(场景5) │ └─ 想做【缓存】?────────► map<string, weak_ptr>(场景6) │ └─ 别人(自己只是找得到它)────────────► weak_ptr / 裸指针简单判断依据:
- 死亡时间由作用域决定 → unique_ptr
- 死亡时间由"最后一人"决定 → shared_ptr
- 自己无权决定生死 → weak_ptr
unique_ptr:独占可移动
同一时刻只能有一个unique_ptr拥有对象,禁止拷贝,只允许移动。是之前的裸指针升级替代版,大部分情况下,都是用unique_ptr,下面是定义方法:
| 类别 | 写法 | 资源来源 |
|---|---|---|
| 一、裸指针接管 | unique_ptr<T> p{new T}; | new表达式 |
unique_ptr<T> p(new T); | new表达式 | |
unique_ptr<T[]> p(new T[n]); | new[] | |
unique_ptr<T, D> p(cApiCreate(), deleter); | C 工厂函数 | |
| 二、make 工厂 | auto p = make_unique<T>(args); | 函数内new |
auto p = make_unique<T[]>(n); | 函数内new[] | |
| 跳过清零 | |
| 三、移动/转换 | unique_ptr<T> p2(std::move(p1)); | 另一个unique_ptr |
unique_ptr<T> p2 = std::move(p1); | 同上 | |
unique_ptr<Base> p = make_unique<Derived>(); | 派生类指针 | |
auto p = factory(); | 工厂函数返回值 | |
| 四、后期绑定 | p.reset(new T(...)); | 运行时决定 |
一、裸指针接管
前三项案例如下,第四项的C工厂函数参考前面的自定义删除器的案例。
class Widget { public: explicit Widget(std::string name) : name_(std::move(name)) { std::cout << " [" << name_ << "] 构造\n"; } ~Widget() { std::cout << " [" << name_ << "] 析构\n"; } void hello() const { std::cout << " [" << name_ << "] hello\n"; } private: std::string name_; }; void demo_from_new() { // 直接初始化 std::unique_ptr<Widget> p(new Widget("甲")); // 花括号括号初始化 std::unique_ptr<Widget> q{new Widget("乙")}; p->hello(); q->hello(); } // 输出析构:乙、甲(后构造先析构) void demo_from_new_array() { // 注意模板参数是 T[],删除器自动变成 delete[] std::unique_ptr<Widget[]> arr(new Widget[3]{ Widget("one"), Widget("two"), Widget("three") }); arr[0].hello(); // 数组版用 operator[] 访问 } // 一次性 delete[] 三个 int main() { // 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲 SetConsoleOutputCP(CP_UTF8); ios::sync_with_stdio(true); cout << "=== main函数正式开始执行 ===" << endl; demo_from_new(); demo_from_new_array(); return 0; } /********打印输出********* === main函数正式开始执行 === [甲] 构造 [乙] 构造 [甲] hello [乙] hello [乙] 析构 [甲] 析构 [one] 构造 [two] 构造 [three] 构造 [one] hello [three] 析构 [two] 析构 [one] 析构 *****************/二、make 工厂
前两项是最常用的用法,std::make_unique的优点在于:①异常安全;②代码里不出现裸new;③ 与make_shared风格统一,案例代码如下:
class Widget { public: explicit Widget(std::string name) : name_(std::move(name)) { std::cout << " [" << name_ << "] 构造\n"; } ~Widget() { std::cout << " [" << name_ << "] 析构\n"; } void hello() const { std::cout << " [" << name_ << "] hello\n"; } private: std::string name_; }; void demo_make() { // C++14 起可用。auto 推导出 unique_ptr<Widget> auto p = std::make_unique<Widget>("旺财"); // 显式类型也行(不推荐,多打字还容易写错) std::unique_ptr<Widget> q = std::make_unique<Widget>("小黑"); p->hello(); } void demo_make_array() { auto arr = std::make_unique<Widget[]>(3); // 3 个元素,值初始化 arr[0].hello(); // 注意:这里只能默认构造(调用 Widget()), // 要带参数构造数组没有 make 版本,得退回 new Widget[3]{...} } int main() { // 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲 SetConsoleOutputCP(CP_UTF8); ios::sync_with_stdio(true); cout << "=== main函数正式开始执行 ===" << endl; demo_make(); demo_make_array(); return 0; } /********打印输出********* === main函数正式开始执行 === [甲] 构造 [乙] 构造 [甲] hello [乙] hello [乙] 析构 [甲] 析构 [one] 构造 [two] 构造 [three] 构造 [one] hello [three] 析构 [two] 析构 [one] 析构 *****************/第三项std::make_unique_for_overwrite用途是跳过memset清零过程,后续直接用read/recv/memcpy把数据写进去,因为马上会被覆盖,所以没必要先清零。如果写成make_unique<char[]>(4096),它会先清零,然后read()又覆盖掉,清零就浪费了。
使用要点
- 只适合马上会整体覆盖写入的场景,比如 IO 缓冲区、网络接收缓冲、文件读取缓冲。
- 不能在写入之前读取缓冲区内容,否则是未定义行为。
- 对于有构造函数的类类型,
make_unique_for_overwrite仍然会调用默认构造函数;它主要省的是 POD/数组的初始化开销。 - 需要 C++20 支持。
可以把它理解成:“我知道这块内存马上会被写满,所以别帮我清零。”
三、移动/转换
前两种是常用的移动构造的方法,一种是圆括号,一种是等号,参见函数demo_move:
#include <iostream> #include <vector> #include <windows.h> #include <memory> using namespace std; class Widget { public: Widget() : name_("默认") { std::cout << " [默认] 构造\n"; } explicit Widget(std::string name) : name_(std::move(name)) { std::cout << " [" << name_ << "] 构造\n"; } ~Widget() { std::cout << " [" << name_ << "] 析构\n"; } void hello() const { std::cout << " [" << name_ << "] hello\n"; } private: std::string name_; }; void demo_move() { auto p1 = std::make_unique<Widget>("原主"); std::cout << "移动前 p1 是否为空: " << (p1 == nullptr) << "\n"; // false // 两种等价写法: std::unique_ptr<Widget> p2(std::move(p1)); // ① 圆括号 // std::unique_ptr<Widget> p2 = std::move(p1);// ② 等号(拷贝初始化,走移动构造) std::cout << "移动后 p1 是否为空: " << (p1 == nullptr) << "\n"; // true! p2->hello(); // 只有 p2 能用 // p1->hello(); // ❌ 空指针解引用,崩溃 } int main() { // 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲 SetConsoleOutputCP(CP_UTF8); ios::sync_with_stdio(true); cout << "=== main函数正式开始执行 ===" << endl; demo_move(); return 0; }关键理解:std::move之后,原指针立刻变空。它不是“复制了一份”,而是“把钥匙交出去了”。
第三种make_unique用途是转换移动:派生类 → 基类(多态工厂的根基)
struct Shape { virtual ~Shape() = default; virtual void draw() const = 0; }; struct Circle : Shape { void draw() const override { std::cout << "○\n"; } }; struct Square : Shape { void draw() const override { std::cout << "□\n"; } }; void demo_convert() { // make_unique<Circle> 返回 unique_ptr<Circle>, // 直接初始化 unique_ptr<Shape> —— 转换移动构造自动完成 std::unique_ptr<Shape> s = std::make_unique<Circle>(); s->draw(); // ○ s = std::make_unique<Square>(); // 移动赋值:先 delete 旧的 ○,再接管 □ s->draw(); // □ } // 只析构 □(○ 早在赋值时就被释放了)这是“工厂返回unique_ptr<Base>”能成立的原因。前提:Shape必须有虚析构函数,否则 delete 走基类指针是 UB。
还有另外一种从工厂函数返回值接收的方式:
std::unique_ptr<Widget> createWidget(std::string name) { return std::make_unique<Widget>(std::move(name)); // 注意:这里【不要】写 return std::move(local)! // 返回局部变量直接 return,编译器做 RVO/移动,写 move 反而阻止优化 } void demo_factory() { // std::unique_ptr<Widget> p = createWidget("工厂货"); // 接收返回值,不需要显式 move p->hello(); }四、后期绑定(定义和使用分离)
先进行空定义,再进行reset重置更新,reset时,会调用之前类的析构函数。
void demo_reset() { std::unique_ptr<Widget> p; // 先空定义(第一类) std::cout << "--- 开始绑定 ---\n"; p.reset(new Widget("绑定一号")); // 接管一个裸指针 p->hello(); std::cout << "--- 换绑 ---\n"; p.reset(new Widget("绑定二号")); // 立即析构一号,接管二号 p->hello(); std::cout << "--- 清空 ---\n"; p.reset(); // 立即析构二号,p 变空 // p = nullptr; // 完全等价的另一种写法 std::cout << "--- 结束 ---\n"; } /* 输出: --- 开始绑定 --- [绑定一号] 构造 [绑定一号] hello --- 换绑 --- [绑定二号] 构造 [绑定一号] 析构 ← reset(new...) 时,旧对象立即释放 [绑定二号] hello --- 清空 --- [绑定二号] 析构 ← reset() 时,立即释放 --- 结束 --- */怎么使用unique_ptr
要创建新对象? ├── 是 ── 对象类型确定 ──────────► make_unique<T>(args) ★默认 │ 要数组 ────────────────► make_unique<T[]>(n) │ 马上整体覆盖的缓冲区 ──► make_unique_for_overwrite<T[]>(n) (C++20) │ 资源来自 C 的 create ──► unique_ptr<T, D>(create(), deleter) │ └── 否 ── 只是需要一个"占位的空指针" ──► unique_ptr<T> p; (后期 reset) 要接管别人的 unique_ptr ────► auto p2 = std::move(p1); 要以基类身份持有派生对象 ──► unique_ptr<Base> p = make_unique<Derived>(); 接收工厂返回值 ────────────► auto p = factory();shared_ptr:共享所有权
unique_ptr是"唯一所有权"(可转移),而shared_ptr是"共享所有权"——内部有一个引用计数,多个shared_ptr指向同一个对象,计数归零时才真正delete。大部分场景默认用unique_ptr,只有在确实需要共享时才升级成shared_ptr。以下是常见的“共享”场景:
1、多方共享配置/资源
一个配置对象被主线程、多个工作线程同时使用。没人“独占”它,谁最后用完,谁负责释放——靠人协调必然出错,shared_ptr让这句话自动成立。
#include <memory> #include <iostream> #include <string> #include <sstream> #include <thread> #include <mutex> #include <vector> std::mutex g_io; // 全局 IO 锁 struct Config { int timeoutMs = 3000; std::string endpoint = "api.example.com"; ~Config() { // 析构函数: std::cout << ">>> Config 被释放了!<<<\n"; } }; // 线程要执行的函数:id 是线程编号,cfg 是【拷贝进来】的 shared_ptr void worker(int id, std::shared_ptr<const Config> cfg) { std::ostringstream oss; // ───────── 进入函数这一刻:cfg 是一份【新的】shared_ptr 副本 ───────── oss << " worker" << id << " 进场,当前计数 = " << cfg.use_count() << ",使用 " << cfg->endpoint; std::lock_guard<std::mutex> lk(g_io); // 函数级加锁 std::cout << oss.str() << '\n'; // 干活:读配置 std::cout << " worker" << id << " 使用 " << cfg->endpoint << std::endl; } // ← 这里 cfg 离开作用域 → 这份副本析构 → 计数 -1 int main() { //创建配置 auto cfg = std::make_shared<const Config>(); // 现在:主线程的 cfg 持有它,计数 = 1 std::cout << "创建完毕,计数 = " << cfg.use_count() << std::endl; // 1 //准备共享配置资源的线程数组 std::vector<std::thread> pool; for (int i = 0; i < 3; ++i) { pool.emplace_back(worker, i, cfg);//带参数创建线程 } //主线程等待所有线程干完 for (auto& t : pool) { t.join(); } // 3 个线程都 join 完 → 它们仨的副本都析构了 std::cout << "线程退出,计数 = " << cfg.use_count() << std::endl; // 1:只剩主线程的 cfg } // ← 主线程的 cfg 在这里离开作用域 → 计数 1 → 0 /**************************** 创建完毕,计数 = 1 worker0 进场,当前计数 = 4,使用 api.example.com worker0 使用 api.example.com worker1 进场,当前计数 = 4,使用 api.example.com worker1 使用 api.example.com worker2 进场,当前计数 = 4,使用 api.example.com worker2 使用 api.example.com 线程退出,计数 = 1 >>> Config 被释放了!<<< ******************************/2、容器间共享元素
游戏里:商店货架上摆着一把铁剑,玩家买下后背包里也有这把剑——是同一把(商店改价,背包里的剑描述同步变化)。
#include <memory> #include <iostream> #include <string> #include <vector> struct Item { std::string name; int price; }; int main() { std::vector<std::shared_ptr<Item>> shop; // 商店货架 std::vector<std::shared_ptr<Item>> bag; // 玩家背包 auto sword = std::make_shared<Item>(Item{"铁剑", 100}); shop.push_back(sword); // 货架上摆着(计数 2) bag.push_back(sword); // 背包里也是它(计数 3) // 关键验证:两个容器里是【同一块内存】 std::cout << "商店改价前,背包里的剑价格: " << bag[0]->price << "\n"; // 80! shop[0]->price = 80; // 商店打折 std::cout << "商店改价后,背包里的剑价格: " << bag[0]->price << "\n"; // 80! // 下架:商店放手,背包里的剑不受影响 shop.clear(); std::cout << "下架后背包剑仍在: " << bag[0]->name << "\n"; } // 背包离开 → 最后一人放手 → 铁剑释放 /*************************** 商店改价前,背包里的剑价格: 100 商店改价后,背包里的剑价格: 80 下架后背包剑仍在: 铁剑 ***************************/3、异步回调保活
把一个任务扔给后台线程/线程池,创建任务的作用域可能早就结束了,但任务执行时还需要那个对象。裸指针必然悬垂,unique_ptr无法拷贝进 lambda——shared_ptr按值捕获是唯一正解。
#include <memory> #include <iostream> #include <string> #include <thread> #include <chrono> #include <functional> struct Job { std::string name; void run() { std::cout << "执行 " << name << "\n"; } ~Job() { std::cout << "销毁 " << name << "\n"; } }; void submit(std::function<void()> task) { std::thread(task).detach(); // 模拟异步:不等它完成 } int main() { { auto job = std::make_shared<Job>(); job->name = "后台任务"; submit([job] { // ★ 按值捕获 shared_ptr:计数 1→2 std::this_thread::sleep_for(std::chrono::milliseconds(100)); job->run(); // 此时 main 的作用域早已结束,但对象活着 }); std::cout << "main 作用域即将结束\n"; } // ← main 里的 job 析构:计数 2→1,对象【没有】死 std::this_thread::sleep_for(std::chrono::milliseconds(300)); } // 线程的 lambda 析构 → 计数 1→0 → Job 此时才销毁 /* 输出: main 作用域即将结束 执行 后台任务 销毁 后台任务 ← 注意:销毁发生在"执行"之后,时间点由任务决定 */以上从打印信息可以看到main先结束,如果从main传递裸指针或者是std::move(unique_ptr)都会异常,只有共享方式才能规避这种异常创建。
| 捕获写法 | 结果 |
|---|---|
[rawPtr](裸指针) | main 出栈后对象已死 → 回调访问悬垂指针,埋雷 |
[up = std::move(uptr)](unique_ptr) | ❌ 多线程下 unique_ptr 不安全(多处析构竞争),且移交后原持有者失能,C++ 标准库不给这条路 |
[job](shared_ptr 按值) | ✅ 计数+1,对象寿命自动延长到回调结束 |
shared_ptr在异步场景的本质:把“对象必须活到回调执行完”从注释里的承诺,变成计数器上的事实。
4、网络会话自保活
网络连接对象Session发起异步读,读完成的回调里需要用到自己。危险在于:回调排队期间,外部可能已经放手(连接被关闭)——回调必须在“自己还活着”的前提下执行。
#include <memory> #include <iostream> #include <thread> #include <chrono> class Session : public std::enable_shared_from_this<Session> { public: void start() { // ★ 在成员函数里给自己"续命":计数+1,回调持有一份 auto self = shared_from_this(); std::thread([self] { std::this_thread::sleep_for(std::chrono::milliseconds(50)); self->onRead(); // 即使外部全部放手,self 保证存活 }).detach(); } void onRead() { std::cout << "读到数据,session 活着\n"; } ~Session() { std::cout << "session 销毁\n"; } }; int main() { auto s = std::make_shared<Session>(); s->start(); s.reset(); // 外部立刻放手:计数 2→1(start 里的 self 还握着) std::cout << "外部已放手\n"; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } /* 输出: 外部已放手 读到数据,session 活着 ← 回调安全执行 session 销毁 ← 回调结束、lambda 析构后才销毁 */weak_ptr:弱引用,不增加引用计数
它不拥有对象,只观察shared_ptr管理的对象是否还活着。使用前必须调用lock()提升成shared_ptr。
#include <memory> #include <iostream> #include <vector> struct Listener { virtual ~Listener() = default; virtual void onEvent(int v) = 0; }; class EventBus { std::vector<std::weak_ptr<Listener>> listeners_; // ★ 弱引用:只观察 public: void subscribe(std::shared_ptr<Listener> l) { listeners_.push_back(l); // weak_ptr 自动从 shared_ptr 构造 } void publish(int v) { for (auto& w : listeners_) if (auto l = w.lock()) // 死了的订阅者:lock 失败,自动跳过 l->onEvent(v); } }; struct UI : Listener { void onEvent(int v) override { std::cout << "UI 收到事件 " << v << "\n"; } ~UI() { std::cout << "UI 销毁\n"; } }; int main() { EventBus bus; { std::shared_ptr ui = std::make_shared<UI>(); bus.subscribe(ui); bus.publish(1); // UI 收到事件 1 bus.publish(2); // UI 收到事件 2 } // UI 销毁(总线拦不住 —— 这正是我们要的语义)- bus.publish(3); // 无输出:死订阅者被静默跳过,不崩溃 std::cout << "发布完毕,无崩溃\n"; }在{}中bus.publish是有效的,离开作用域之后,ui已经销毁,但是bus.publish(3);并不会报错,可见它的“观察者”属性,而不是“占有者”(独占/共享)属性。