了解Objective-C/Swift的程序员应该知道引用计数的概念。引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
在传统C++中,程序员需要手动释放资源,经常忘记去释放资源而导致泄露。通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII资源获取即初始化技术。
传统C++里我们使用new
和delete
去申请和释放资源,两者必须配对写,new
会返回一个裸指针,即Object *
这种形式。而C++11引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。 这些智能指针包括:
shared_ptr
unique_ptr
weak_ptr
使用它们需要包含头文件memory
,下面进行详细介绍。
shared_ptr
shared_ptr
是一种智能指针,它能够记录多少个shared_ptr
共同指向一个对象,从而消除显式的调用delete
,当引用计数变为零的时候就会将对象自动删除。
make_shared
用来消除显式的使用new
,它会分配创建传入参数中的对象,并返回这个对象类型的shared_ptr
指针。shared_ptr
可以通过get()
方法来获取原始指针,通过reset()
来减少一个引用计数, 并通过use_count()
来查看一个对象的引用计数。
下面是关于 shared_ptr
的示例:
特点
- 占用内存高:因为除了要管理一个裸指针外,还要维护一个引用计数器。
- 原子操作性能低:虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
使用场景
- 通常使用在共享权不明的场景,有可能多个对象同时管理同一个内存。
- 对象的延迟销毁,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用
BlockingQueue<shared_ptr<void>>
将对象转移到另外一个线程中释放,从而解放关键线程(陈硕-《Linux多线程服务器端编程》)。
unique_ptr
unique_ptr
是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。unique_ptr
只支持移动,不支持赋值:
1 | unique_ptr<int> pointer = make_unique<int>(10); |
下面是关于 unique_ptr
的示例:
特点
unique_ptr
在默认情况下和裸指针的大小是一样的。所以内存上没有任何的额外消耗,性能是最优的。
使用场景
- 忘记
delete
:如果因为一些原因,Fold code blockCPP Copy codew
必须建立在堆上。如果用裸指针管理w
,那么需要在析构函数中delete w
,但程序员容易漏写该语句,造成内存泄漏。
如果按照unique_ptr
的写法,不用在析构函数手动delete
属性。当对象析构时,属性w
将会自动释放内存。
- 异常安全
假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:在正常流程下,我们会在函数末尾Fold code blockCPP Copy code1
2
3
4
5
6void process()
{
Widget* w = new Widget();
w->do_something(); // 可能会发生异常
delete w;
}delete
创建的对象w
,正常调用析构函数,释放内存。
但是如果w->do_something()
发生了异常,无法执行到delete w
。此时就会发生内存泄漏。
我们当然可以使用try…catch
捕捉异常,在 catch
里面执行delet
,但是这样代码上并不美观,也容易漏写。
如果我们用unique_ptr
,那么这个问题就迎刃而解了。无论代码怎么抛异常,在unique_ptr
离开函数作用域的时候,内存就将会自动释放。
weak_ptr
看如下代码;
运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露,如图所示:
解决这个问题的办法就是使用弱引用指针weak_ptr
,它是一种弱引用(相比较而言shared_ptr
就是一种强引用)。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如图下图所示:
在上图中,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。
weak_ptr
没有*
运算符和->
运算符,所以不能够对资源进行操作,它可以用于检查shared_ptr
是否存在,其expired()
方法能在资源未被释放时,会返回false
,否则返回true
;除此之外,它也可以用于获取指向原始对象的shared_ptr
指针,其lock()
方法在原始对象未被释放时,返回一个指向原始对象的shared_ptr
指针,进而访问原始对象的资源,否则返回nullptr
。
总结
在日常使用中,unique_ptr
使用频率最高,weak_ptr
最低,需要避免循环引用的情况。当然,还是要根据具体的业务场景和性能要求来选择哪种指针。