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_ptr
s比对应的std::shared_ptr
s活得更久的情况也不建议使用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> |