C++中已经基本不推荐使用裸指针(手动进行new和delete),基本上都是使用智能指针。所以作为C++程序员必须学会智能指针的使用:auto_ptr(已废弃不建议使用)、unique_ptr、share_ptr、weak_ptr

auto_ptr(已废弃)
auto_ptr是C++98标准库中提供的智能指针之一,用于管理动态分配的内存资源。它采用独占所有权的方式管理资源,即同一时间只能有一个auto_ptr实例拥有特定资源的所有权。当auto_ptr实例被销毁时,它会自动释放所管理的资源,从而避免了内存泄漏的问题。
1 | auto_ptr<string> sp1(new string("hello world")); |
auto_ptr被废弃有一下几个问题:
- 语义不清。当复制一个
auto_ptr对象时(拷贝复制或operator =复制),原对象所持有的堆内存对象也会转移给复制出来的对象。这种是复制语义,但是具体实现却是转移(move),语义上就容易让人混淆,让人逻辑上很容易错误,所以后面出现了unique_ptr代替auto_ptr - 不适用于容器:
auto_ptr不能很好地与STL容器一起使用,因为在容器中进行元素的复制或移动操作时会导致指针所有权的转移,可能引发错误。
auto_ptr了解一下就行,主要感觉八股文考的多一些
unique_ptr
unique_ptr是C++11标准引入的智能指针之一,它采用独占所有权的方式,即同一时间只能有一个unique_ptr实例拥有特定资源的所有权。这意味着在任何时刻只有一个unique_ptr可以指向特定的资源,避免了多重所有权可能引发的问题。unique_ptr可以说是auto_ptr的升级版本。
前面auto_ptr代码使用unique_ptr代替:
1 | std::unique_ptr<std::string> uptr1(new std::string("hello world")); |
auto_ptr的前车之鉴,unique_ptr禁止复制语义,为了达到这个效果,std::unique_ptr类的拷贝构造函数和赋值运算符(operator =)被标记为delete。unique_ptr只能通过移动构造或者移动赋值进行转移- 自定义删除器。
unique_ptr默认的删除器是delete,但是有时候我们想定义自己的删除器,这个也是可以实现的。
1 |
|
unique_ptr的指针大小和普通指针相等,所以一般没有特殊要求,我们可以使用unique_ptr最节约内存。
源码分析
unique_ptr其中的源码(visual studio C++20):
1 | template <class _Ty, class _Dx /* = default_delete<_Ty> */> |
unique_ptr实现粗略看比较简单,数据成员比较重要的就是_Mypair,里面存放者删除器和指针,在析构的时候就调用删除器删除指针。
share_ptr
share_ptr通过引用计数的方式来控制资源的释放,允许多个指针指向同一个资源(共享),每多一个std::shared_ptr对资源的引用,资源引用计数将增加1,每一个指向该资源的std::shared_ptr对象析构时,资源引用计数减1,最后一个std::shared_ptr对象析构时,发现资源计数为0,将释放其持有的资源。
1 | //初始化方式1 |
- 自定义删除器,
shared_ptr自定义删除器比unique_ptr简单一些,不会影响指针指针的类型
1 |
|
- 在
shared_ptr有两个指针,一个指针指向资源,一个指针指向控制块,所以shared_ptr大小其实是普通指针的两倍,并且每次shared_ptr操作都会对控制块进行操作(对引用技术进行操作),所以效率没有unique_ptr高。

源码分析
先看类图

shared_ptr继承_Ptr_base,关于指针和控制块的部分都是在_Ptr_base中,shared_ptr调用_Ptr_base中内容进行引用计算的增加和减少_Ptr_base重要的数据成员:_Ptr、_Rep两个指针一个指向资源、一个指向控制块(_Ref_count_base)_Ref_count_base(控制块)是个纯虚类,里面有uses和weaks两个引用控制块_Ref_count_base有三个实现类_Ref_count、_Ref_count_resource、_Ref_count_resource_alloc,分别代表普通引用计数、带删除器的引用计数、带删除器和分配器的引用计数
1 | template <class _Ty> |
1 | template <class _Ty> |
1 | class __declspec(novtable) _Ref_count_base { // common code for reference counting |
1 | // 不带删除器的引用计数 |
weak_ptr
std::weak_ptr 是 C++ 标准库中的智能指针,用于解决 std::shared_ptr 的循环引用(circular reference)问题。std::weak_ptr 是一种弱引用,不会增加引用计数,也不会拥有被管理的对象。它通常用于解决由 std::shared_ptr 形成的循环引用导致的内存泄漏问题,简单来说weak_ptr就是用来配合share_ptr一起进行使用
1 |
|
通过前面share_ptr源码的理解,我们也能猜测weak_ptr是通过控制块中weaks进行控制的,从源码角度来说weak_ptr代码里面应该是对weaks进行控制,当有一个新的weak_ptr指向share_ptr式,weaks数量进行增加,
所以内存释放来说:
- 当
Uses为0时,share_ptr指向资源会进行释放 - 当
weaks为0时,控制块内存才最终进行了释放
1 | template <class _Ty> |
make_unique和make_shared
现代C++建议多使用make_unique和make_shared,原因主要是两点
- 性能优势:
std::make_shared可以在单个内存分配中同时分配控制块和对象内存,而直接使用std::shared_ptr则需要分别进行两次内存分配。这样可以提高内存访问的效率并减少内存碎片。(仅仅针对make_shared) - 异常安全性:
std::make_shared可以确保在内存分配失败时不会泄漏内存,因为它是原子操作,要么成功创建std::shared_ptr,要么不创建,不会出现中间状态。
第二点异常安全性的解释:
假设我们有个函数按照某种优先级处理Widget:1
void processWidget(std::shared_ptr<Widget> spw, int priority);
现在假设我们有一个函数来计算相关的优先级,1
int computePriority();
并且我们在调用processWidget时使用了new而不是std::make_shared:1
2processWidget(std::shared_ptr<Widget>(new Widget), //潜在的资源泄漏!
computePriority());
在运行时,一个函数的实参必须先被计算,这个函数再被调用,所以在调用processWidget之前,必须执行以下操作,processWidget才开始执行:
- 表达式
new Widget必须计算,例如,一个Widget对象必须在堆上被创建 - 负责管理
new出来指针的std::shared_ptr<Widget>构造函数必须被执行 computePriority必须运行
编译器不需要按照执行顺序生成代码。new Widget必须在std::shared_ptr的构造函数被调用前执行,因为new出来的结果作为构造函数的实参,但computePriority可能在这之前,之后,或者之间执行。也就是说,编译器可能按照这个执行顺序生成代码:
- 执行
new Widget - 执行
computePriority - 运行
std::shared_ptr构造函数
如果按照这样生成代码,并且在运行时computePriority产生了异常,那么第一步动态分配的Widget就会泄漏。因为它永远都不会被第三步的std::shared_ptr所管理了。
使用std::make_shared可以防止这种问题。调用代码看起来像是这样:在运行时,1
2processWidget(std::make_shared<Widget>(), //没有潜在的资源泄漏
computePriority());std::make_shared和computePriority其中一个会先被调用。如果是std::make_shared先被调用,在computePriority调用前,动态分配Widget的原始指针会安全的保存在作为返回值的std::shared_ptr中。如果computePriority产生一个异常,那么std::shared_ptr析构函数将确保管理的Widget被销毁。如果首先调用computePriority并产生一个异常,那么std::make_shared将不会被调用,因此也就不需要担心动态分配Widget(会泄漏)。
当然除了使用std::make_shared<Widget>(),还有其他解法比如:但是能够一行代码解释清楚最好还是使用一行代码,所以还是建议使用1
2std::shared_ptr<Widget> spw(new Widget);
processWidget(spw, computePriority()); // 正确,但是没优化,见下make_shared
make_shared不适用场景
- 需要自定义删除器。make函数不能自定义删除器
- 需要精确精确的分配、释放对象大小的内存(重载了
operator new和operator delete)的类,因为make_shared,因为make_shared使用std::allocate_shared进行内存分配,std::allocate_shared需要的内存总大小不等于动态分配的对象大小,还需要再加上控制块大小 - 对于大对象来说,
std::weak_ptrs比对应的std::shared_ptrs活得更久的情况也不建议使用make_shared。因为make_shared申请的内存是包括资源和内存块的内容,只有最后一个std::shared_ptr和最后一个指向它的std::weak_ptr已被销毁,整块内存才会释放。而普通的方式创建的shared_ptr,最后一个shared_ptr被销毁就将对象的内存销毁了,控制块的还保留
1 | class ReallyBigType { … }; |
1 | class ReallyBigType { … }; //和之前一样 |
enable_share_from_this
实际开发中,有时候需要在类中返回包裹当前对象(this)的一个std::shared_ptr对象给外部使用,这个时候我们就需要enable_shared_from_this。
1 |
|
源码分析:
enable_shared_from_this中有一个_Wptr,将_Wptr和this对应的share_ptr进行关联,从而后续通过shared_from_this从此_Wptr得到share_ptr
1 | template <class _Ty> |
那么这个是什么时候将_Wptr和share_ptr 关联的呢?是通过_Set_ptr_rep_and_enable_shared函数
1 | explicit shared_ptr(_Ux* _Px) { // construct shared_ptr object that owns _Px |
_Set_ptr_rep_and_enable_shared函数中会进行判断,如果是_Can_enable_shared就会关联
1 | void _Set_ptr_rep_and_enable_shared(_Ux* const _Px, _Ref_count_base* const _Rx) noexcept { // take ownership of _Px |
_Can_enable_shared的实现使用了SFINAE
1 | template <class _Yty, class = void> |
线程安全
先说结论智能指针线程不安全。
share_ptr有两部分指向资源的指针 和指向控制块的指针,其中控制块部分中引用计数是原子的操作可以说它是安全的,其他部分都没有保障,比如指针的指向。
所以智能指针是线程不安全的

想要对智能指针安全操作可以参考muduo代码:


智能指针自己的实现
unique_ptr实现
1 | template<typename T> |
share_ptr的实现
1 | template<typename T> |