尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
【C++开发】从裸指针到智能指针
前言老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?spm1001.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?spm1001.2014.3001.5501在语法层次上LIFO 模型管理的是“变量”如果其中一个变量是裸指针当裸指针所在作用域结束时裸指针也会被直接释放掉如果在裸指针被释放之前开发人员通过new申请了堆资源动态内存但没有用delete释放堆资源动态内存,那么这个堆资源就会一直存在这就是内存泄漏常见的场景。RAII优化 LIFO 模型LIFO 模型抽象本身只保证“自动变量的存储随作用域失效”它并不天然管理堆内存、文件句柄这些非栈资源这也是在使用裸指针申请堆内存时必须显示释放堆内存才能规避内存泄漏的风险出现异常退出时不能完全规避这种风险。但如果引入RAII机制以类替代裸指针入栈释放堆内存交由类的析构函数来完成。这样的好处就是①不需要显示释放堆内存②异常退出情况下也不会出现内存泄漏的风险出栈时必定会调用这个类的析构函数释放资源。以上描述“以类替代裸指针入栈”的特征就是智能指针的特性。智能指针本身就是一个类按LIFO 模型资源出栈作用域结束 / 异常展开 / 函数返回时必定会通过智能指针的析构函数释放堆内容以下是流程图栈上对象unique_ptrT8字节 │ 出栈作用域结束 / 异常展开 / 函数返回 ▼ 析构函数 ~unique_ptr() │ ▼ delete ptr_ ──────► 堆上的 T 对象被销毁智能指针的本质一个单成员裸指针的类模板把“堆内存的释放”这一行代码搬进了析构函数从而把一个不受作用域管辖的资源绑到一个严格受 LIFO 管辖的对象上。unique_ptr是它的最简形式零开销delete 直挂shared_ptr是它的多路复用形式释放动作变成“原子计数归零判定”且自己管理着第二层资源——控制块。以下是GCC libstdcbits/unique_ptr.h的真实代码templatetypename _Tp, typename _Dp default_delete_Tp class unique_ptr { // 数据成员不是裸的 pair而是一个 tuple —— 暗藏 EBO 优化见下文 tuplepointer, 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_uniqueMyClass(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_ptrMyClass, void(*)(MyClass*); void demo1() { // 构造时必须同时传【对象指针】和【删除器函数】 MyClassPtr p(createMyClass(), destroyMyClass); //std::unique_ptrMyClass, void(*)(MyClass*) p( // createMyClass(), // destroyMyClass // 自定义删除器函数指针方式 //); p-printMessage(hello); } // 析构 → get_deleter()(ptr) → 通过函数指针调用 destroyMyClass(ptr) → delete obj常用智能指针类型类型适用场景注意事项unique_ptr独占资源、成员变量、工厂返回不能拷贝只能moveshared_ptr多所有者共享有控制块和原子计数开销weak_ptr观察、缓存、打破循环引用使用前必须lock()用法流程图对象死亡时间由谁决定 │ ├─ 当前作用域函数/类自己管──────────► unique_ptr ← 默认 │ ├─ 最后一个使用者多方共享───────────► shared_ptr │ │ │ ├─ 共享者是【回调/线程】──► 按值捕获进 lambda场景3/4 │ ├─ 共享者是【多个容器】──► 容器存 shared_ptr场景2 │ ├─ 共享者想【只读快照】──► shared_ptrconst T / COW场景1/7 │ ├─ 有【互指/回指】─────► 从属方改 weak_ptr场景5 │ └─ 想做【缓存】────────► mapstring, weak_ptr场景6 │ └─ 别人自己只是找得到它────────────► weak_ptr / 裸指针简单判断依据死亡时间由作用域决定 → unique_ptr死亡时间由最后一人决定 → shared_ptr自己无权决定生死 → weak_ptrunique_ptr独占可移动同一时刻只能有一个unique_ptr拥有对象禁止拷贝只允许移动。是之前的裸指针升级替代版大部分情况下都是用unique_ptr下面是定义方法类别写法资源来源一、裸指针接管unique_ptrT p{new T};new表达式unique_ptrT p(new T);new表达式unique_ptrT[] p(new T[n]);new[]unique_ptrT, D p(cApiCreate(), deleter);C 工厂函数二、make 工厂auto p make_uniqueT(args);函数内newauto p make_uniqueT[](n);函数内new[]auto p make_unique_for_overwriteT[](n);跳过清零三、移动/转换unique_ptrT p2(std::move(p1));另一个unique_ptrunique_ptrT p2 std::move(p1);同上unique_ptrBase p make_uniqueDerived();派生类指针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_ptrWidget p(new Widget(甲)); // 花括号括号初始化 std::unique_ptrWidget q{new Widget(乙)}; p-hello(); q-hello(); } // 输出析构乙、甲后构造先析构 void demo_from_new_array() { // 注意模板参数是 T[]删除器自动变成 delete[] std::unique_ptrWidget[] 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() { // C14 起可用。auto 推导出 unique_ptrWidget auto p std::make_uniqueWidget(旺财); // 显式类型也行不推荐多打字还容易写错 std::unique_ptrWidget q std::make_uniqueWidget(小黑); p-hello(); } void demo_make_array() { auto arr std::make_uniqueWidget[](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_uniquechar[](4096)它会先清零然后read()又覆盖掉清零就浪费了。使用要点只适合马上会整体覆盖写入的场景比如 IO 缓冲区、网络接收缓冲、文件读取缓冲。不能在写入之前读取缓冲区内容否则是未定义行为。对于有构造函数的类类型make_unique_for_overwrite仍然会调用默认构造函数它主要省的是 POD/数组的初始化开销。需要 C20 支持。可以把它理解成“我知道这块内存马上会被写满所以别帮我清零。”三、移动/转换前两种是常用的移动构造的方法一种是圆括号一种是等号参见函数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_uniqueWidget(原主); std::cout 移动前 p1 是否为空: (p1 nullptr) \n; // false // 两种等价写法 std::unique_ptrWidget p2(std::move(p1)); // ① 圆括号 // std::unique_ptrWidget 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_uniqueCircle 返回 unique_ptrCircle // 直接初始化 unique_ptrShape —— 转换移动构造自动完成 std::unique_ptrShape s std::make_uniqueCircle(); s-draw(); // ○ s std::make_uniqueSquare(); // 移动赋值先 delete 旧的 ○再接管 □ s-draw(); // □ } // 只析构 □○ 早在赋值时就被释放了这是“工厂返回unique_ptrBase”能成立的原因。前提Shape必须有虚析构函数否则 delete 走基类指针是 UB。还有另外一种从工厂函数返回值接收的方式std::unique_ptrWidget createWidget(std::string name) { return std::make_uniqueWidget(std::move(name)); // 注意这里【不要】写 return std::move(local)! // 返回局部变量直接 return编译器做 RVO/移动写 move 反而阻止优化 } void demo_factory() { // std::unique_ptrWidget p createWidget(工厂货); // 接收返回值不需要显式 move p-hello(); }四、后期绑定定义和使用分离先进行空定义再进行reset重置更新reset时会调用之前类的析构函数。void demo_reset() { std::unique_ptrWidget 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_uniqueT(args) ★默认 │ 要数组 ────────────────► make_uniqueT[](n) │ 马上整体覆盖的缓冲区 ──► make_unique_for_overwriteT[](n) (C20) │ 资源来自 C 的 create ──► unique_ptrT, D(create(), deleter) │ └── 否 ── 只是需要一个占位的空指针 ──► unique_ptrT p; 后期 reset 要接管别人的 unique_ptr ────► auto p2 std::move(p1); 要以基类身份持有派生对象 ──► unique_ptrBase p make_uniqueDerived(); 接收工厂返回值 ────────────► 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_ptrconst Config cfg) { std::ostringstream oss; // ───────── 进入函数这一刻cfg 是一份【新的】shared_ptr 副本 ───────── oss worker id 进场当前计数 cfg.use_count() 使用 cfg-endpoint; std::lock_guardstd::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_sharedconst Config(); // 现在主线程的 cfg 持有它计数 1 std::cout 创建完毕计数 cfg.use_count() std::endl; // 1 //准备共享配置资源的线程数组 std::vectorstd::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::vectorstd::shared_ptrItem shop; // 商店货架 std::vectorstd::shared_ptrItem bag; // 玩家背包 auto sword std::make_sharedItem(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::functionvoid() task) { std::thread(task).detach(); // 模拟异步不等它完成 } int main() { { auto job std::make_sharedJob(); 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_thisSession { 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_sharedSession(); s-start(); s.reset(); // 外部立刻放手计数 2→1start 里的 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::vectorstd::weak_ptrListener listeners_; // ★ 弱引用只观察 public: void subscribe(std::shared_ptrListener 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_sharedUI(); 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);并不会报错可见它的“观察者”属性而不是“占有者”独占/共享属性。
RELATED

相关推荐

【Linux网络】网络基础下篇:数据到底怎么从进程 A 到进程 B?从 MAC、IP、端口到 Socket 一次理清

【Linux网络】网络基础下篇:数据到底怎么从进程 A 到进程 B?从 MAC、IP、端口到 Socket 一次理清

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C、 《Linux操作系统》 🐶学习方向:C方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然 🏠博主简介 文章目录前…

📅 2026/10/26 1:59:07
【基于 Swoole+Hyperf 的微服务实战】第十二周·周四:安全模块:IP 黑白名单、SQL 注入/XSS 检测插件、请求体大小限制

【基于 Swoole+Hyperf 的微服务实战】第十二周·周四:安全模块:IP 黑白名单、SQL 注入/XSS 检测插件、请求体大小限制

【基于 SwooleHyperf 的微服务实战】第十二周周四:安全模块:IP 黑白名单、SQL 注入/XSS 检测插件、请求体大小限制今天我们进入第十二周周四,主题是 安全模块:IP 黑白名单、SQL 注入/XSS 检测插件、请求体大小限制。在前面几天&am…

📅 2026/10/26 1:54:07
部署实战(三):ICP备案通过后,从HTTPS证书签发到微信小程序域名配置

部署实战(三):ICP备案通过后,从HTTPS证书签发到微信小程序域名配置

部署实战系列 📌 部署实战(一):2核2G轻量服务器 Docker 部署 Java 全栈及小程序上线避坑指南 部署实战(一):2核2G轻量服务器Docker部署Java全栈及小程序上线避坑指南-CSDN博客 📌 部…

📅 2026/10/26 1:54:07
MORE NEWS

更多资讯

📰

从完美主义到无可挑剔:一套可复制的高质量行动法

1. 从“完美主义”到“无可挑剔”:重新理解这个词我第一次认真琢磨“impeccable”这个词,不是因为词典,而是因为收到过一封邮件。那封邮件不长,但措辞、排版、附件命名、签名档甚至发送时间,都让人觉得发件人极其靠谱。…

📰

ADS8688+STM32F407+RT-Thread高精度模拟量采集系统实战

简介:本资源是一套基于STM32F407主控、ADS8688高精度ADC与RT-Thread实时操作系统的完整嵌入式驱动工程,面向嵌入式开发初学者及RTOS进阶实践者,解决高精度模拟信号采集在实时系统中稳定驱动与数据处理的典型难题,适用于工业传感、…

📰

代码质量量化落地:三层机制让“完美”不再只是口号

我一直觉得,软件团队里最容易被误解的一个词就是“完美”。很多人一听“完美代码”,第一反应是“这不可能”“这是洁癖”,或者“我们要的是能上线,不是搞艺术”。但几年前我在一个模拟项目里认真地追了一次“完美”,项…

📰

SXSSFWorkbook分页查询实现百万行Excel流式导出

一次导出请求把服务拖垮,事后排查下来,数据库不慢、网络不抖,问题出在几百兆的单元格对象全堆在堆内存里。SXSSFWorkbook 实现分页查询导出这个组合,就是专门解决这类场景的:流式写出把内存中的行数压到固定窗口&#…

📰

对话模型记忆增强实战:claude-mem架构解析

很多人在使用 Claude 这类对话模型时都遇到过同一个尴尬:上午刚聊完的方案细节,下午换个话题再切回来,它已经完全不记得了。如果只是临时问答倒也还好,可一旦你希望它持续跟进一个项目、记住你的写作偏好、或者跨多轮会话维护一份…

📰

AI编程中的规则约束:提升代码质量与工程效能

1. AI编程时代的规则约束革命当代码补全工具开始接管我们的IDE时,一个被忽视的真相正在浮现:真正决定AI编程质量的不是算法本身,而是我们为它设定的规则边界。最近三个月,我的团队在Vibe-Coding工作流中系统性地引入了Rules约束机…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬