现代C++ [1]: 智能指针
简介
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象(类)。当栈对象的生存周期结束后(离开对象的作用域,如函数结束),会在析构函数中释放掉申请的内存,不需要手动释放内存空间,从而规避内存泄漏的风险。
auto_ptr
auto_ptr
是C++ 98标准的方案,在C++ 11标准中已经弃用,采用独占所有权模式,看下面这个例子:
std::auto_ptr<string> p1 (new string ("Hello World")); |
auto_ptr
的赋值操作在编译时不会报错,但p2
剥夺了p1
的所有权,使得p1
成为一个空指针。所以当程序运行时访问p1
将会报错。这也是促使auto_ptr
的被弃用的原因。
unique_ptr
作为接替auto_ptr
的独占资源所有权的智能指针,其特点如下:
- 独占式拥有或严格拥有, 保证同一时间内只有一个智能指针可以指向该对象;
- 常见用法:使用 std::unique_ptr 自动管理内存,基本的RAII(Resource Acquisition Is Initialization)思想;
{ |
- 采用独占所有权模式,不允许左值赋值操作,但可以接受临时右值(引用)的赋值,即这里的
operator=
不是copy赋值操作符,而是move赋值操作符;
std::unique_ptr<string> p1 (new string ("Hello World")); |
其内在逻辑是左值(拷贝)赋值操作p2 = p1;
后,无法合理的处理左值p1
的去留。若采用auto_ptr
的思路,将p1
置为nullptr,则会重走带来潜在的访问null指针而程序崩溃风险的老路,若置之不理,则不满足独占的属性,所以干脆就不允许这样做。而右值赋值则不存在这样的问题,右值本身就是临时的,在赋值给左值后就会被销毁,左值仍然独占资源。std::move
就是将一个左值转换成右值引用,从而实现移动语义。移动语义的存在就是在强调转移所有权,即这时候你是清楚地知道p1
对资源的所有权发生了转移,所以即便还是出现了p1
已经被销毁,不再可用的情况,那么后面乱用p1
而导致程序崩溃的代价也应当由乱用者来承担。
- 可以指向一个数组;
// 实测C++14标准下,以下两种写法都支持 |
- 可以自定义deleter;
{ |
- 可以使用lambda函数形式的deleter;
{ |
shared_ptr
std::shared_ptr
是引用计数型智慧指针(reference-counting smart pointers,RCSP),其特点如下:
- 共享式拥有,多个智能指针可以指向相同的对象,该对象和其相关资源会在最后一个指向它的指针被销毁时被释放;
- 使用计数机制来表明资源被几个指针共享。可以通过成员函数
use_count()
来查看资源的所有者个数;
{ |
- 可以指向一个数组;
// 实测 std::make_shared<int[]> 这种写法直到C++20标准才支持 |
- 可以自定义deleter;
{ |
- 常用的成员函数:
-
use_count
:返回引用计数的个数; -
unique
:返回是否为独占所有权( use_count 为 1),C++17标准已弃用; -
swap
:交换两个 shared_ptr 所拥有的对象; -
reset
:放弃拥有对象的所有权或拥有对象的变更(参数为一个新的对象,表示放弃原对象,转而指向新对象),会引起原有对象的引用计数的减少; -
get
:返回内部对象的裸指针,;
-
- 出于性能和异常安全的考虑,尽量使用make_shared进行初始化,详细讨论见这里。
weak_ptr
share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效(两个指针的引用计数永远不可能下降为0),从而导致内存泄漏。可以看下面这个例子:
class B; //声明 |
weak_ptr 是为了配合shared_ptr而引入的智能指针,它是一种不控制对象生命周期的智能指针, 指向一个 shared_ptr 管理的对象,只可以从一个shared_ptr或另一个weak_ptr对象构造,是shared_ptr的附属。因此,真正管理对象的还是强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段,类似于观察者的角色,weak_ptr的构造和析构不会引起引用记数的增加或减少,weak_ptr也无法直接访问对象的成员方法,除非将其转化成强引用的shared_ptr。其特点还有:
- weak_ptr和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给weak_ptr,weak_ptr可以通过调用lock成员函数来转化为shared_ptr;
- weak_ptr可以用来解决shared_ptr相互引用的死锁问题;
class B; //声明 |
-
常用的成员函数:
expired
:用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false;- weak_ptr 没有重载
operator*
和operator->
,所以不能通过weak_ptr直接访问对象的方法,比如B
类的对象中有一个方法print()
,我们不能这样访问:pa->pb_->print()
; lock
:用于获取所管理的对象的强引用shared_ptr. 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象的指向与 weak_ptr 相同。故可以用如下方式访问B
类对象的print()
方法:
shared_ptr<B> p = pa->pb_.lock();
p->print();use_count
:返回与 shared_ptr 共享的对象的引用计数;reset
:将 weak_ptr 置空;- weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数;
参考
[1] 详解C++11智能指针
[2] 现代 C++:一文读懂智能指针