C++ 14 智能指针

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011808673/article/details/80814454

C++中通过new创建一个对象,通过delete销毁一个对象;在合适的时机销毁一个对象是及其困难的(特别是在多线程环境下),有时候我们会忘记delete而造成内存泄漏,有时候我们又会在尚有使用者的情况下销毁一个对象,造成使用者对内存的非法访问;在多线程程序中,存在了太多的竞态条件,当你把一个原始指针暴露给别的线程,问题就来了,安全的销毁它并通知其它线程的使用者该指针失效是非常困难的,虽然极端情况的竞态条件发生概率不高,但对于长期运行的程序或是频繁操作的场景,如果处理不好,销毁了一个正在被其它线程使用的对象是必然要发生的(这种情况下最好的结果是程序崩溃)。

C++ 11提供的智能指针,使上面诸多问题迎刃而解,可以说智能指针是C++ 11提供的最为有用的特性之一,它让C++程序员可以像提供GC或ARC机制的语言(java等对象语义的语言都属此类)那样管理动态创建的对象,以对象语义(复制引用而非值)复制和传递对象,当且仅当没有任何使用者时(引用计数为零时),对象才会被销毁(并且立即销毁),大大简化了手动管理动态内存的困难。

shared_ptr

我们首先介绍智能指针中最常用的一类shared_ptr,它是类模板,在memory头文件中提供。与很多C++ 11特性一样,它先在boost库和tr1中提供。声明shared_ptr必须指明它包含的裸指针的类型,和其它类模板一样,在尖括号中给出类型,比如:

智能指针未被初始化之前,它只包含一个空指针(nullptr),可以通过bool运算符判断一个智能指针是否为空,如果为空,bool运算符返回false,可以这样写,把智能指针直接写在if的条件表达式里:

智能指针的初始化主要有两种方式:

1.把原生指针作为参数传给智能指针的构造函数,也可以通过其他shared_ptr拷贝构造,比如:

2.使用std::make_shared函数模板,这是最为安全的方式,下面的写法和上面是等效的:

make_shared定义于memory头文件,它会用接受的参数调用相应的构造函数创建新的对象。

shared_ptr可以像普通指针一样使用,解引用可以返回它所包含的对象,也可以通过->访问它所包含的对象的成员函数或变量。

shared_ptr通过引用计数的方式管理它所包裹的动态对象,当它被复制的时候(拷贝构造或拷贝赋值),引用计数会加1,当它析构、reset()或被其他智能指针赋值时,引用计数会减1,当引用计数为0时,智能指针会自动释放它所管理的对象。它的工作原理可以参考本系列的第3篇里一个简单的模拟实现;

由此可见,只要一个智能指针对象不为空,你就不用担心它所包裹的对象在别处被释放(因为引用计数至少为1),可以放心的使用;另外,程序员也再也不用纠结在哪里释放一个动态资源(特别是这个资源被多处使用时),智能指针会在“最后的时刻”自动释放它;包裹同一个动态对象的智能指针都是平权的,最初的智能指针也没有更多的特权,谁最后释放对它的持有,谁就负责销毁它,这就是shared_ptr的名字的来历。

我们举个例子,看看智能指针使用时,引用计数的变化情况,引用计数可以通过shared_ptr的成员函数use_count读取,代码如下:

运行输出为:

第一行,只有初始化的智能指针一个引用;

第二行,lambda对象对智能指针进行了值捕获,引用计数加1;

第三行,lambda对象销毁,引用计数减一,同时refFunction形参类型是引用类型,不会发生拷贝,所以引用计数还是1;

第四行,valFunction形参类型是值类型,智能指针发生了复制,引用计数加1;

当倒数第二个大括号结束时,strPtr对象析构,析构函数把引用计数减一,同时发现引用计数已经为0,因此销毁string对象。

uniq_ptr

顾名思义,uniq_ptr持有对对象的独有权——两个unique_ptr不能指向一个对象,不能进行复制操作只能进行移动操作。它也定义于memory头文件,如果某个动态对象的从属非常明确,可以用uniq_ptr代替shared_ptr(很显然,它比shared_ptr轻量,也不会被意外共享)。uniq_ptr析构、reset()或者被赋值时,它当前持有的对象就会被销毁。比如:

即使动态对象的作用域非常明确,可以直接new和delete,使用智能指针也是有额外好处的,比如:

这两段代码原理上是等效的,但是,如果第一段在new和delete之间的程序抛出了异常,执行路径发生变化,delete语句没有机会执行,就会发生内存泄漏;而第二段程序就没有这种危险,只要脱离tmp的作用域,uniq_ptr的析构函数就会处理好内存的释放。显然使用智能指针达到了更安全的目的。这种通过对象的生命周期管理资源的方式被称为RAII(Resource Acquisition Is Initialization),是C++中非常好的编程思想,智能指针本质上都是RAII思想的体现,RAII以后我们还会遇到。

定制删除器

智能指针默认的资源销毁操作是调用delete操作符,我们也可以定制自己的deleter,智能指针创建时作为最后一个参数传入,智能指针将在销毁资源是调用我们定制的deleter。deleter是一个可调用对象(参见第2篇),传入的参数是该智能指针包裹的动态资源。定制deleter一般用于如下的方面:

1. 动态内存是数组资源时,应该使用delete []销毁,这时使用delete操作符会出问题,而shared_ptr没有针对数组的特化版本(unique_ptr有),所以使用shared_ptr管理数组资源时,要自定义deleter,比如:

最新公布的C++17标准,shared_ptr也可以管理数组资源了

2.上一条推而广之,只要智能指针管理的资源不是new创建的,我们就需要指定一个deleter。比如一个文件指针,通过fopen创建,通过fclose关闭,如果用智能指针管理就需要定制deleter,如下:

3.如果想在释放动态资源时,再做一些额外的操作时,也需要通过自定义deleter实现,这里不再举例了。


shared_ptr的线程安全

上一篇我们简单说了一下shared_ptr的线程安全问题,这里我们再稍微仔细讲一下:

shared_ptr并不是100%线程安全的,它的引用计数本身是线程安全的(而且无锁,性能很高),但是对象的读写不是,shared_ptr的线程安全级别和内建类型、标准容器、std::string等一样,即:

一个shared_ptr对象可以被多个线程同时读取,比如在多个线程中位于赋值操作符右边,或是解引用、->等操作;

两个不同的shared_ptr对象,即使他们管理同一个对象指针,也可以在不同的线程中同时写入,比如被赋值、被移动、析构、reset等等操作;

如果要在多个线程中对同一个shared_ptr对象进行读写,那么需要加锁保证线程安全;

第三种情况我们应该在编程实践中尽量避免,方案是复制shared_ptr对象保证每个线程持有自己的对象,这样读写互不影响。实在非要在不同线程中读写同一个智能指针对象,记得要加锁。

另外请注意,这里讨论的是shared_ptr对象本身的线程安全级别,不是它管理的对象的线程安全级别,shared_ptr不对也不应该对它管理的资源的线程安全负责。



展开阅读全文

没有更多推荐了,返回首页