C++从C++11开始引入了智能指针(std::unique_ptr、std::shared_ptr、std::weak_ptr),并后面的各个版本中对智能指针进行了改进。
unique_ptr
unique_ptr为独占所有权的智能指针,不允许拷贝构造和赋值构造,仅支持std::move移动。
从C++14 起支持使用 std::make_unique 方式构造 unique_ptr,推荐使用std::make_unique 方式。
1 |
|
删除器
std::unique_ptr的默认行为是使用 delete 或 delete[] 来释放其管理的资源。但当你需要管理非传统资源(如文件句柄、数据库连接)或需要特殊的释放逻辑时,自定义删除器就变得至关重要。
1 | _EXPORT_STD template <class _Ty, class _Dx /* = default_delete<_Ty> */> |
从上面的unique_ptr模板类的定义可以知道,需要通过模板的第二个参数 _Dx 来指定自定义的删除器。unique_ptr的删除器通过模板参数指定,在编译期确定,这样做的好处是通常没有额外的运行时开销,但灵活性较差。
unique_ptr的删除器可以有多种指定方式,如函数对象、函数指针、std::function、lamada 表达式。
1 |
|
shared_ptr
std::shared_ptr 基于引用计数,允许多个 shared_ptr 共享同一对象的所有权。每当一个 shared_ptr 被拷贝时,引用计数加 1(可以使用use_count成员函数获取引用计数)。每当一个 shared_ptr 被销毁或重置时,引用计数就会减 1。当引用计数降为 0 时,所管理的对象会被自动删除。
1 | // 若在 new 和 shared_ptr构造间发生异常,可能出现内存泄漏 |
只有在需要自定义删除器、对内存释放时机有极其苛刻的要求、处理私有构造函数或必须使用花括号初始化列表这些少数特定场景下,才考虑直接使用 new来构造 std::shared_ptr。
拷贝
支持拷贝和移动,每拷贝一次,引用计数加 1:
1 | shared_ptr<Foo> sp3 = sp1; // sp1和sp3共享所有权,引用计数加1 |
移动
支持使用 std::move 进行所有权移动,移动后,源 shared_ptr 变为 nullptr,但新shared_ptr的引用计数与之前一样:
1 | cout << "sp3 use_count: " << sp3.use_count() << endl; // 2 |
删除器
当需要自定义删除器时,只能使用 new 来构造shared_ptr,不能使用 make_shared。
shared_ptr 的删除器与unique_ptr不同,其不是通过模板参数指定的,而是通过函数参数指定的,因此是运行时动态存储的,虽然灵活性较高,但性能会有轻微影响。
shared_ptr 的删除器支持函数对象、lamada表达式、函数指针等形式,通过第2个参数指定。
1 | std::FILE* fp = std::fopen("test.txt", "w"); |
循环引用
当两个或多个对象互相持有对方的 shared_ptr,会导致引用计数永远无法归零,从而内存泄漏。如下面场景:
1 | struct Node { |
解决方案:将其中一个指针改为 std::weak_ptr。
1 | struct Node { |
weak_ptr
std::weak_ptr 是 C++ 智能指针体系中一个非常独特的角色,其必须从 std::shared_ptr 或另一个 std::weak_ptr创建,不能独立存在,主要用于解决共享所有权带来的循环引用问题。
1 |
|
std::weak_ptr的核心价值在于它不拥有对象的所有权,不会增加引用计数。
使用weak_ptr可以解决shared_ptr带来的两个关键问题:
循环引用 (Circular Reference):当两个或多个对象相互持有对方的 std::shared_ptr时,它们的引用计数永远无法降为零,导致内存泄漏。使用 std::weak_ptr替代其中一个引用可以打破循环。
观察者模式 (Observer Pattern):观察者不应控制被观察对象的生命周期。使用 std::weak_ptr允许观察者检查对象是否还存在,而不会阻止其被销毁。
必须使用 lock()方法来尝试获取一个有效的 std::shared_ptr,然后判断并使用。
1 | if (auto tempSharedPtr = weakPtr.lock()) { // lock() 返回一个临时的 shared_ptr |