五星 C++ 智能指针的分类、实现原理、易出现的问题
智能指针是高频考察内容;本文讲述了智能指针的分类,实现原理以及容易出现的问题及其解决方式。
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 <memory>
头文件中。
C++11 中智能指针包括以下三种:
共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过
use_count()
查看资源的所有者的个数,可以通过unique_ptr
、weak_ptr
来构造,调用release()
释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用
move()
函数),即一个unique_ptr
对象赋值给另一个unique_ptr
对象,可以通过该方法进行赋值。弱指针(weak_ptr):指向
share_ptr
指向的对象,能够解决由shared_ptr带来的循环引用问题。
智能指针的实现原理是计数原理。
在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
1、共享指针(shared_ptr)
1、初始化
共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count。如果我们不初始化一个智能指针,他会被初始化为一个空指针 。
2、获得原始地址
利用get()获得原地址;对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的get()方法得到原始地址
3、指定删除器
当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
1 |
|
另外,我们还可以自己封装一个函数模板make_shared_array方法来让shared_ptr支持数组,代码如下:
1 |
|
最安全的分配和使用动态内存的方法是调用make_shared标准库函数。此函数在动态内存中分配一个对象并从初始化它,返回指向此对象的shared_ptr。
1 | shared_ptr<int> ptr1 = make_shared<int> (42); |
我们不能将一个内置指针转换为一个智能指针:
1 | shared_ptr<int> ptr1 = new int(100);//这种是错误的 |
2、独占指针 (unique_ptr)
1、初始化
std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。不过我们可以通过move函数,将一个独占指针的值赋给另一个独占指针。
独占指针的初始化必须采用直接初始化方式,不支持拷贝,也不支持赋值。
正确的初始化:
1 | unique_ptr<int> ptr; |
错误的初始化:
1 | unique_ptr<int> ptr(ptr1); //错误,不支持拷贝 |
2、删除器
问题:如何将一个unique_ptr赋值给另一个unique_ptr?
A:可以利用std::move,其目的是实现所有权的转移。
1 | // A 作为一个类 |
1 |
|
3、弱指针 (weak_ptr)
弱引用智能指针std::weak_ptr可以看做是shared_ptr的助手,它不管理shared_ptr内部的指针。std::weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在。
1、初始化
1 |
|
weak_ptr<int> wp1;构造了一个空weak_ptr对象
weak_ptr<int> wp2(wp1);通过一个空weak_ptr对象构造了另一个空weak_ptr对象
weak_ptr<int> wp3(sp);通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象
wp4 = sp;通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象(这是一个隐式类型转换)
wp5 = wp3;通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象
通过调用std::weak_ptr类提供的use_count()方法可以获得当前所观测资源的引用计数
2、常用函数
通过调用std::weak_ptr类提供的expired()方法来判断观测的资源是否已经被释放
通过调用std::weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象
通过调用std::weak_ptr类提供的reset()方法来清空对象,使其不监测任何资源
- 利用**
weak_ptr可以解决shared_ptr的一些问题
**
- 返回管理this的shared_ptr
 2. 解决循环引用问题
4、使用是智能指针会出现什么问题?
循环引用;
两个类内的循环指针互相指向,没有指向环中的外部共享指针,shared_ptr
引用计数无法抵达 0,该调用析构函数但是没能调用,导致了内存泄漏。
使用弱指针,可以避免这个问题:
weak_ptr
对被shared_ptr
管理的对象存在非拥有性引用,在访问所引用的对象前必须先转化为shared_ptr
;weak_ptr
用来打断shared_ptr
所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr
引用计数无法抵达0,内存被泄露;令环中的指针之一为弱指针可以避免该情况。weak_ptr
用来表示临时所有权的概念。当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用weak_ptr
跟踪该对象;需要获得所有权时将其转化为shared_ptr
,此时如果原来的shared_ptr
被销毁,则该对象的生命周期被延长到这个临时的shared_ptr
同样被销毁。
未使用弱指针:
起初定义完ptr_a
和ptr_b
时,只有1,3两条引用,即ptr_a
指向CA对象,ptr_b
指向CB对象。然后调用函数set_ptr()
后又增加了2,4两条引用,即CB对象中的m_ptr_a
成员变量指向CA对象,CA对象中的m_ptr_b
成员变量指向CB对象。
这个时候,指向CA对象的有两个,指向CB对象的也有两个。当main函数运行结束时,对象ptr_a
和ptr_b
被销毁,也就是1,3两条引用会被断开,2,4两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。
2,4之间的引用,m_ptr_a指向CA,CA不为空,m_ptr_a有效,不能被释放掉。同时由于m_ptr_a的存在,CB不为空,m_ptr_b不能被释放掉。这就导致了这两个相互引用的次数一直为1,不能被释放,计数机制失效了,造成了内存泄漏。
使用弱指针:
流程与上一例子大体相似,但是不同的是4这条引用是通过weak_ptr
建立的,并不会增加引用计数。也就是说,CA的对象只有一个引用计数,而CB的对象只有两个引用计数,当main函数返回时,对象ptr_a
和ptr_b
被销毁,也就是1,3两条引用会被断开,此时CA对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题。
5、使用new和delete管理动态内存的缺点(坚持使用智能指针)
使用new和delete管理动态内存容易存在问题:
1、忘记用delete 释放内存
2、同一块内存由于拷贝,可能被释放了两次
3、可能会使用释放后的对象
因此,坚持使用智能指针,可以避免所有这些问题,对于一块内存,只有在没有任何指针指向的时候,才会释放他。