
智能指针深入C进阶计划 · Day 3 | 预计学习时长2小时引言Day 1 我们讲了移动语义Day 2 讲了完美转发——两者本质上都是关于资源的正确转移。今天我们把目光投向资源的生命周期管理这是现代 C 最核心的实践之一。如果你写了七年 QtQSharedPointer、QScopedPointer肯定不陌生。但标准库的智能指针在实现细节、语义约束和性能特征上和 Qt 版有不少差异。搞懂这些差异才能在 Qt 项目和纯 C 项目之间切换自如也才能理解为什么现代 C 社区推荐裸指针只做观察者不做拥有者。核心概念1.unique_ptr独占所有权的零开销抽象unique_ptr表达的是唯一所有权不可复制只能移动。它是智能指针中最重要的那个。内存布局unique_ptrT: ┌──────────┐ │ pointer │ → 指向对象 └──────────┘ unique_ptrT[], D (D有状态时): ┌──────────┐ │ pointer │ → 指向数组首元素 │ deleter │ (如果 D 是空类通过 EBO 不占用额外空间) └──────────┘ 实际上 unique_ptr 的实现通过继承或压缩对删除器进行存储 有状态的删除器会占用额外空间但空删除器通过 EBO 实现零开销。空删除器的 EBOEmpty Base Optimization默认删除器std::default_deleteT是空类unique_ptr利用 EBO 将它作为基类因此sizeof(unique_ptrT) sizeof(T*)真正的零开销。static_assert(sizeof(std::unique_ptrint)sizeof(int*));// 通常通过有状态的删除器则不同structFileCloser{voidoperator()(FILE*f)const{if(f)fclose(f);}intlog_fd-1;// 有状态};// sizeof(unique_ptrFILE, FileCloser) sizeof(FILE*)移动语义std::unique_ptrWidgetp1std::make_uniqueWidget();std::unique_ptrWidgetp2std::move(p1);// 所有权转移// p1 现在 nullptrp2 持有对象函数返回unique_ptr利用 RVO/NRVO 可能零开销std::unique_ptrWidgetcreateWidget(){returnstd::make_uniqueWidget();// NRVO零开销}数组特化autoarrstd::make_uniqueint[](10);// C14创建10个int的数组arr[0]42;arr[9]100;// 有效索引 0-9// 注意make_uniqueint[](10) 会值初始化所有元素// 对于 int所有元素初始化为 0// 如果需要默认初始化不置零使用 new int[10]unique_ptrT[]提供operator[]但不提供operator*和operator-防止指针算术误用。经验法则优先用vector而非unique_ptrT[]除非有明确的性能/接口约束。2.shared_ptr共享所有权的引用计数方案shared_ptr允许多个指针共享同一个对象的所有权通过引用计数控制生命周期。内存布局重点栈上的 shared_ptr: ┌──────────────┐ │ ptr_ │ ──→ 对象 (Object) │ ctrl_block_ │ ──→ 控制块 (Control Block) └──────────────┘ 控制块 (Control Block): ┌──────────────────────┐ │ strong_count (long) │ ← shared 引用计数 │ weak_count (long) │ ← weak 引用计数包括弱引用数 1如果 strong_count 0 │ vptr │ ← 虚函数表指针删除器和分配器擦除 │ [deleter存储] │ ← 自定义删除器 │ [allocator存储] │ ← 自定义分配器 └──────────────────────┘补充说明weak_count 的实际值 weak_ptr 数量 (strong_count 0 ? 1 : 0)。这个额外的 1 是为了在 strong_count 变为 0 时控制块仍能存活到所有 weak_ptr 释放。make_shared 的优势在于对象和控制块在同一块内存但缺点是当 strong_count 0 且 weak_count 0 时对象内存无法释放。shared_ptr的开销来源两次内存分配一次给对象一次给控制块除非用make_shared引用计数的原子操作每次拷贝/析构都要atomic fetch_add/fetch_sub控制块本身的内存至少 3 个long vptrmake_shared的优化// 两次分配autosp1std::shared_ptrWidget(newWidget);// 一次分配对象和控制块在同一块内存中autosp2std::make_sharedWidget();make_shared的内存布局┌─────────────────────────────┐ │ Control Block │ │ ├─ strong_count │ │ ├─ weak_count │ │ ├─ vptr │ │ ├─ deleter │ ← 可能存储在控制块内或外部 │ ├─ allocator │ ← 可能存储在控制块内或外部 ├─────────────────────────────┤ │ Widget 对象 │ ← 紧跟控制块之后同一块内存 └─────────────────────────────┘补充说明控制块的具体布局是实现定义的标准只规定行为不规定布局。不同标准库libstdc、libc、MSVC STL的实现细节可能不同。上图只是概念性示意。make_shared的陷阱当有weak_ptr指向对象时即使所有shared_ptr都释放了控制块仍需保持weak_count 0导致 Widget 对象的内存也无法释放——它们在同一块内存里。std::weak_ptrWidgetwk;{autospstd::make_sharedWidget();// 一次分配wksp;}// sp 析构但 Widget 内存不会释放wk 仍存在控制块不能销毁// Widget 已析构~Widget 已调用但内存未归还wk.expired();// true// 直到 wk 析构整块内存才被释放这是make_shared在长生命周期weak_ptr场景下的反直觉行为。如果对象很大考虑用new分离分配。3.weak_ptr打破循环引用的观察者weak_ptr不增加引用计数是对shared_ptr管理对象的弱引用。autospstd::make_sharedWidget();std::weak_ptrWidgetwpsp;// 使用前必须 lock()返回 shared_ptr 或 nullptrif(autolockedwp.lock()){locked-doSomething();}else{// 对象已被销毁}循环引用经典案例structNode{std::shared_ptrNodenext;std::shared_ptrNodeprev;// ← 循环引用~Node(){std::coutNode destroyed\n;}};autoastd::make_sharedNode();autobstd::make_sharedNode();a-nextb;b-preva;// a↔b 循环引用两者永远不会析构// 修复prev 改为 weak_ptrstructNode2{std::shared_ptrNode2next;std::weak_ptrNode2prev;// ✓ 弱引用不增加计数};4. 自定义删除器unique_ptr的删除器删除器是模板参数的一部分编译期确定对于空删除器通过 EBO 实现零运行时开销和零额外内存占用。对于有状态的删除器会占用额外内存且可能引入运行时开销如果删除器有虚函数等。// RAII 管理文件句柄structFdDeleter{voidoperator()(int*fd)const{if(fd*fd0){close(*fd);deletefd;}}};usingunique_fdstd::unique_ptrint,FdDeleter;// RAII 管理C API指针structCurlDeleter{voidoperator()(CURL*curl)const{curl_easy_cleanup(curl);}};usingunique_curlstd::unique_ptrCURL,CurlDeleter;// lambda作为删除器autodeleter[](FILE*f){if(f)fclose(f);};std::unique_ptrFILE,decltype(deleter)file(fopen(test.txt,r),deleter);shared_ptr的删除器删除器是运行时擦除的通过控制块中的函数指针或虚函数机制不影响shared_ptr的类型。这意味着不同删除器的 shared_ptr 类型相同可以放在同一个容器中。补充说明标准库实现通常使用类型擦除技术控制块是一个模板类包含删除器的具体类型但 shared_ptr 本身只持有指向控制块基类的指针。这样无论删除器是什么类型shared_ptr 的类型始终是 shared_ptr。// 不同删除器但类型相同可以放在同一个容器里autosp1std::shared_ptrFILE(fopen(a.txt,r),fclose);autosp2std::shared_ptrFILE(fopen(b.txt,r),[](FILE*f){fclose(f);log(file closed);});std::vectorstd::shared_ptrFILEfiles{sp1,sp2};// OK这是shared_ptr比unique_ptr灵活的地方但代价是虚函数调用和额外存储。5. 性能对比特性unique_ptrshared_ptr裸指针拷贝开销禁止移动允许原子引用计数1指针复制移动开销指针赋值指针赋值引用计数不变)指针复制析构开销直接 delete原子计数-1条件delete无内存占用1个指针2个指针控制块1个指针删除器开销编译期(EBO)运行时(虚函数)手动线程安全构造/析构线程安全引用计数原子操作线程安全无shared_ptr多线程下的原子操作开销显著——cache line bouncing 能让性能差一个数量级。代码实战实战1Qt 对象的智能指针适配Qt 对象通常有父子关系自动管理内存但有时需要脱离 Qt 树独立管理// ❌ 给 Qt 对象配 shared_ptr双重管理易出问题// auto btn std::make_sharedQPushButton(Click); // 危险// ✓ 方式1用 unique_ptr 管理手动设置 parent 前转移所有权autobtnstd::make_uniqueQPushButton(Click);btn-setParent(parentWidget);btn.release();// 转移给 Qt 对象树管理// ✓ 方式2自定义删除器最佳实践structQObjectDeleter{voidoperator()(QObject*obj)const{if(obj!obj-parent()){deleteobj;}// 如果有 parentobj 会被 Qt 父对象树管理不需要 delete// 注意如果 obj 有 parent直接 return 可能会导致内存泄漏不会// 因为父对象析构时会 delete 所有子对象。// 但这里有一个潜在问题如果 obj 有 parentunique_ptr 析构时不 delete// 父对象树会负责删除这是正确的。}};重要警告如果 QObject 有 parent它会在父对象析构时被自动删除。如果 unique_ptr 不知道这一点即删除器直接 delete就会发生双重删除double-free。上面的删除器通过检查 parent() 来避免这个问题。实战2线程安全的对象池#includememory#includemutex#includevector#includefunctionaltemplatetypenameTclassObjectPool{public:std::shared_ptrTacquire(){std::lock_guardstd::mutexlock(mutex_);T*rawnullptr;if(pool_.empty()){rawnewT();}else{rawpool_.back();pool_.pop_back();}// 注意lambda 捕获 this 是安全的因为 shared_ptr 析构时// 如果 this 已被销毁会导致未定义行为// 解决方案使用弱引用或确保 ObjectPool 生命周期长于所有 shared_ptrreturnstd::shared_ptrT(raw,[this](T*p){std::lock_guardstd::mutexlock(mutex_);pool_.push_back(p);});}~ObjectPool(){for(auto*p:pool_)deletep;}private:std::vectorT*pool_;std::mutex mutex_;};实战3用weak_ptr实现安全的回调注册classSubscriber{public:virtual~Subscriber()default;virtualvoidonEvent(conststd::stringmsg)0;};classEventEmitter{public:voidsubscribe(std::weak_ptrSubscribersub){subscribers_.push_back(std::move(sub));}voidemit(conststd::stringmsg){autoitsubscribers_.begin();while(it!subscribers_.end()){if(autospit-lock()){sp-onEvent(msg);it;}else{itsubscribers_.erase(it);// ✅ 正确erase 返回下一个有效迭代器}}}private:std::vectorstd::weak_ptrSubscribersubscribers_;};// 使用classMySub:publicSubscriber{public:voidonEvent(conststd::stringmsg)override{std::coutReceived: msg\n;}};EventEmitter emitter;{autosubstd::make_sharedMySub();emitter.subscribe(sub);emitter.emit(hello);// 输出: Received: hello}// sub 析构emitter.emit(world);// 无输出weak_ptr::lock() 返回 nullptr自动清理常见陷阱与最佳实践陷阱1shared_ptr的线程安全误解// ❌ 引用计数是线程安全的但指向的对象不是autospstd::make_sharedint(0);// 线程1: *sp 1; ← 数据竞争// 线程2: *sp 1; ← 数据竞争正确理解shared_ptr的控制块引用计数操作是线程安全的shared_ptr指向的对象不是线程安全的// 多个线程读同一个 shared_ptr 对象拷贝它是安全的// 前提是没有线程同时修改这个 shared_ptr 对象本身// ❌ 错误多个线程同时修改同一个 shared_ptr 对象std::shared_ptr sp std::make_shared(0);// 线程1: sp std::make_shared(1); // 写操作// 线程2: sp std::make_shared(2); // 写操作 → 数据竞争// ✅ 正确每个线程操作自己的 shared_ptr 副本std::shared_ptr sp std::make_shared(0);std::thread t1([sp] { // 捕获副本auto local sp; // 拷贝引用计数1安全});std::thread t2([sp] { // 捕获副本auto local sp; // 拷贝引用计数1安全});核心规则多个线程读取同一个 shared_ptr 对象调用 get()、operator* 等是安全的多个线程拷贝同一个 shared_ptr 对象是安全的引用计数原子操作多个线程修改同一个 shared_ptr 对象赋值、reset()是不安全的需要外部同步多个线程通过 shared_ptr 访问指向的对象是不安全的需要外部同步陷阱2enable_shared_from_this的误用classBad:publicstd::enable_shared_from_thisBad{public:std::shared_ptrBadgetSelf(){returnshared_from_this();// 如果对象不是由 shared_ptr 管理 → 抛 std::bad_weak_ptr 异常}};classGood:publicstd::enable_shared_from_thisGood{public:staticstd::shared_ptrGoodcreate(){autoptrstd::shared_ptrGood(newGood());// 注意不能在构造函数中调用 shared_from_this()// 因为此时 shared_ptr 还未构造完成returnptr;}std::shared_ptrGoodgetSelf(){returnshared_from_this();// OK对象已由 shared_ptr 管理}private:Good()default;// 私有构造强制使用 create()};shared_from_this()//在以下情况下会抛出 std::bad_weak_ptr//对象是栈上分配的//对象是静态/全局的//对象由 unique_ptr 管理//在构造函数中调用此时 weak_ptr 尚未初始化//在析构函数中调用此时控制块已开始销毁// ❌ 栈对象调用Bad b;b.getSelf();// std::bad_weak_ptr 异常// ✓ 堆对象由 shared_ptr 管理autopstd::make_sharedBad();p-getSelf();// OK陷阱3unique_ptr的删除器类型传染// 不同删除器 不同类型autof1std::unique_ptrint,FdDeleter(newint(open(...)));autof2std::unique_ptrint,decltype([](int*p){close(*p);deletep;})(newint(open(...)));// f1 和 f2 类型不同不能放在同一个容器里需要统一类型时要么用shared_ptr类型擦除要么用类型别名。最佳实践汇总默认用unique_ptr共享需求出现时才用shared_ptr总是用make_unique/make_shared创建避免异常安全问题和额外分配裸指针只做观察者函数参数T*或T表示不获取所有权weak_ptr打断循环引用双向链表、观察者模式、缓存Qt 对象优先用父子关系管理智能指针只管理无父对象enable_shared_from_this只在对象确实需要传shared_ptr给外部时用避免shared_ptr的全局/长期存储——引用计数会让对象赖着不死进阶思考shared_ptr与QSharedPointer差异特性std::shared_ptrQSharedPointer删除器构造时传入类型擦除默认delete可传入自定义删除器weak_ptr独立类型std::weak_ptrQWeakPointerQt6 中已弃用推荐使用 std::weak_ptr 或 QPointerQt集成无可qobject_cast转型make_shared优化有无始终两次分配补充说明Qt6 的变化QWeakPointer 在 Qt6 中已被弃用deprecated推荐使用 std::weak_ptr 用于 std::shared_ptr 的弱引用对于 QObject 的弱引用使用 QPointer它不依赖引用计数对于 QSharedPointer使用 QWeakPointer 仍然有效但不推荐在新代码中使用迁移建议新代码优先std::shared_ptr需要qobject_cast时用QSharedPointer。延伸方向std::observer_ptrC26 proposal显式我不拥有比裸指针更语义化侵入式引用计数boost::intrusive_ptr对象自身持有计数减少控制块开销std::shared_mutexshared_ptr读写锁与共享所有权的结合模式参考资源cppreference: std::unique_ptrcppreference: std::shared_ptrHoward Hinnant: unique_ptr is not just for RAIIHerb Sutter: GotW #89 - Smart PointersEffective Modern C- Item 18-22 (Scott Meyers)C Concurrency in Action- 6.1节shared_ptr与线程安全 (Anthony Williams)