本章深入探讨了 C++11 引入的四种智能指针:std::unique_ptr、std::shared_ptr、std::weak_ptr 以及过时的 std::auto_ptr,旨在帮助开发者摆脱原始指针带来的内存管理噩梦。
条款十八:对于独占资源使用 std::unique_ptr
std::unique_ptr 是管理独占资源的首选,高效且零开销。
独占所有权:拥有资源的唯一所有权,不可拷贝(只可移动),析构时自动释放资源。
零开销抽象:默认情况下,大小等同于原始指针,操作指令也基本一致。
适用场景:
工厂函数返回类型:工厂函数创建对象后,由调用者接管所有权。
std::unique_ptr可以高效地转换为std::shared_ptr,赋予调用者极大的灵活性。Pimpl 惯用法:用于隐藏实现细节(详见条款二十二)。
自定义删除器:
支持自定义资源释放逻辑(如写日志后再 delete)。
类型即定义:删除器类型是
std::unique_ptr类型的一部分(std::unique_ptr<T, DeleterType>),这可能导致对象尺寸增加(尤其是使用函数指针或有状态的函数对象时)。推荐使用无状态 Lambda:相比函数指针,无状态 Lambda 不会增加
std::unique_ptr的大小。
数组支持:提供
std::unique_ptr<T[]>特化版本,但通常建议使用std::vector或std::array。
条款十九:对于共享资源使用 std::shared_ptr
std::shared_ptr 通过引用计数实现共享所有权,提供类似垃圾回收的自动化内存管理。
共享所有权:多个指针共同拥有一个对象,最后一个指针销毁时释放资源。
性能开销:
内存:大小通常是原始指针的两倍(包含指向对象的指针和指向控制块的指针)。
动态分配:引用计数存储在动态分配的控制块(Control Block)中。
原子操作:引用计数的增减必须是原子操作,以保证线程安全,这比普通整数运算慢。
控制块(Control Block):
包含引用计数、弱引用计数(weak count)、自定义删除器、自定义分配器等。
创建规则:
std::make_shared、从独占指针构造、从原始指针构造时会创建控制块。陷阱:避免从同一个原始指针创建多个
std::shared_ptr,这会导致多个控制块和重复析构(Double Free)。应直接传递new的结果给构造函数,或使用std::make_shared。
自定义删除器:
删除器不是
std::shared_ptr类型的一部分,存储在控制块中。这意味着不同删除器的
std::shared_ptr可以互相赋值或放入同一个容器。
std::enable_shared_from_this:当需要在类成员函数中安全地创建指向
this的std::shared_ptr时使用。通过继承该模板类并调用
shared_from_this(),避免重复创建控制块。
条款二十:当 std::shared_ptr 可能悬空时使用 std::weak_ptr
std::weak_ptr 是 std::shared_ptr 的辅助者,用于观测资源但不持有所有权。
不影响引用计数:
std::weak_ptr指向由std::shared_ptr管理的对象,但不增加对象的引用计数(即不阻止对象被销毁)。解决悬空指针问题:能够检测所指对象是否已被销毁(
expired())。使用方法:
不能直接解引用。
必须通过
lock()获取std::shared_ptr来访问对象(如果对象存在),或检查是否为空。
典型应用场景:
缓存:缓存对象但不阻止其被回收。
观察者模式:Subject 持有 Observer 的弱引用,避免循环引用和悬空访问。
打破循环引用:在
A和B互相引用的场景中,一方使用weak_ptr可打破死锁,允许资源被正常回收。
条款二十一:优先考虑使用 std::make_unique 和 std::make_shared,而非直接使用 new
推荐使用 make 函数系列(std::make_unique、std::make_shared)来创建智能指针。
优势:
代码简洁:避免了类型名称的重复书写(如
std::unique_ptr<Widget>(new Widget)vsstd::make_unique<Widget>())。异常安全:防止在
new对象和智能指针构造之间发生异常(如函数参数求值顺序导致的泄漏),make函数将这两步合并为一个原子操作。性能提升(仅
std::make_shared):std::make_shared会分配单块内存同时存放对象和控制块,减少了内存分配次数和开销。
不适用场景:
自定义删除器:
make函数不支持指定自定义删除器。花括号初始化:
make函数内部使用圆括号完美转发,无法直接使用花括号初始化列表(需通过auto变通)。内存敏感的大对象(仅
std::shared_ptr):使用std::make_shared时,由于对象和控制块在同一内存块,只要有weak_ptr存在(控制块存活),整个大对象的内存就无法释放,即使强引用计数已归零。自定义内存管理:类重载了
operator new/delete时,make函数的分配行为可能不符合预期。
条款二十二:当使用 Pimpl 惯用法,请在实现文件中定义特殊成员函数
Pimpl(Pointer to Implementation)惯用法用于减少编译依赖。
问题:在使用
std::unique_ptr实现 Pimpl 时,如果在头文件中使用默认析构函数,会导致编译错误(“不完整类型”错误)。原因:
std::unique_ptr的默认删除器在编译期检查类型完整性(sizeof),而头文件中Impl类尚未定义。
解决方案:
在头文件中声明析构函数。
在实现文件(
.cpp)中,在Impl类定义之后,定义析构函数(即使只是default)。
移动操作:移动构造和移动赋值运算符也面临同样问题,需要在实现文件中定义。
拷贝操作:
std::unique_ptr仅支持移动,若需支持拷贝(深拷贝 Pimpl),必须手动实现拷贝构造和赋值运算符。std::shared_ptr的不同:std::shared_ptr不受此限制,因为其删除器是运行时绑定的,不需要在声明时检查类型完整性。
评论