|
|
这几年给新人做代码走查,发现 C++ 智能指针常被“学会了语法、没吃透语义”。用得顺手和真正理解生命周期、所有权、开销与坑,有着不小的距离。下面围绕 shared_ptr、unique_ptr、weak_ptr 和自定义删除器,聊聊我在实战里的取舍与踩坑。
先说 unique_ptr。它本质是独占所有权的 RAII 容器,零额外计数开销,移动不复制。设计上非常符合“资源有且仅有一个主人”的直觉。工程里,凡是对象不会被共享,就优先 unique_ptr:构造时用 make_unique,成员字段直接 unique_ptr,接口要转移所有权就以 unique_ptr 作为参数或返回值。这样不仅省去引用计数,也迫使调用方在语义上明确“谁负责销毁”。常见误区是为了“怕悬垂”把一切都塞进 shared_ptr,这会把简单的所有权问题,升级为计数同步与循环依赖问题。
shared_ptr 的强大在于共享所有权与跨作用域传递,但代价也明显:引用计数的原子增减、控制块额外分配,以及潜在的循环引用。性能敏感路径上,应尽量以 const T&/T* 传读,不要无脑拷贝 shared_ptr。如果确实需要共享所有权,优先 make_shared,它能把对象与控制块打包一次分配(小对象尤其受益)。例外是需要自定义删除器或自定义分配器时,make_shared 不一定合适。另外,别让 this 直接逃逸成 shared_ptr;应继承 enable_shared_from_this,通过 shared_from_this 获取与现有控制块一致的 shared_ptr,否则会制造“双重控制块”,导致对象被析构两次或永不析构。
循环引用是 shared_ptr 的经典陷阱。两边
互持 shared_ptr 会让计数永不归零。典型例子是“父子互指”:Parent 持有 Child 的 shared_ptr,Child 又反指 Parent 的 shared_ptr,结果内存泄漏还伴随析构不触发的资源泄露。解决方式是把反向关系降级为 weak_ptr:父强持有子,子用 weak_ptr 观测父,需要用时 lock 成临时 shared_ptr。weak_ptr 不参与计数,能打破环。顺带一提,weak_ptr.lock() 失败是正常路径,别把它当异常;逻辑上要接受“对象可能已经消失”,在接口层体现出可空与重试。
自定义删除器经常被忽视,却是把智能指针接到“非 new 资源”的关键。比如 FILE*、自定义句柄、需要特定关闭序列的对象:用 unique_ptr 或 shared_ptr(ptr, Deleter) 绑定正确的释放逻辑,异常/早退都能安全收尾。注意 shared_ptr 的删除器类型不进控制块类型签名(但会存储一份),而 unique_ptr 的删除器类型参与类型系统,这会影响对象大小与可移动性。若删除器是无状态的函数对象,建议用空类 + noexcept 的 operator(),编译器可实现零开销。还有一点,别把“原始 this 指针”随手包成 shared_ptr 并附删除器,这与 enable_shared_from_this 的控制块语义冲突,风险极高。
说到接口设计,我更倾向于“谁创建谁销毁,谁拥有谁表达”。调用方不共享就传 T&/T*(不拥有),需要可空就用裸指针或 span/optional 的组合;需要转移就用 unique_ptr;确实要共管才用 shared_ptr。返回对象时,new 出来的就直接返回 unique_ptr,避免“先裸指针后包裹”的窗口期。字段成员尽量 unique_ptr 或值语义,shared_ptr 只出现在“确实有共享生命周期”的图结构或缓存层。跨线程共享对象时,shared_ptr 的原子计数是线程安全的,但这不等于对象本身是线程安全的;对象的内部同步依然需要你自己实现或保证只读。
性能与内存占用上,有几点实参数据给我深刻教训:在热点循环里频繁复制 shared_ptr,原子加减能把 L3 打成筛子;把只读访问改为传 const T* 或 const Ref 后,尾延迟直接腰斩。另一个是 make_shared 带来的控制块合并,能显著减少碎片与 TLB 压力;但如果对象里有 weak_from_this 的需求或需要精确控制销毁顺序(比如先释放外部资源,再减计数),有时分配分离更安全。还有,shared_ptr 的别名构造函数可以让你持有控制块却指向子对象,避免额外子对象生命周期管理,这在“返回切片视图”时很有用,但务必确保主对象活得够久。
最后谈心智模型:智能指针不是“更安全的指针”,而是“把生命周期决策前置到类型层”。一旦你在图纸上画清楚所有权流向,代码层自然就能选出 unique_ptr、shared_ptr、weak_ptr 的合适组合;反之,拿 shared_ptr 当“保险丝”,只会把问题埋到运行时。建议团队层面统一约定:默认 unique_ptr,禁止把 this 包装成新的 shared_ptr,双向关系一侧必须 weak_ptr,热点路径严禁无谓的 shared_ptr 复制,并在评审里画出所有权图。链接里 cppreference 的智能指针条目与 Herb Sutter 的“Leak-free by default”文章值得通读,搜索关键字即可找到相关页面,例如在 cppreference.com 上检索 shared_ptr/unique_ptr/weak_ptr 的条目和示例。 |
|