其实在之前学习C语言,C++时,我们只有使用指针,并没有涉及智能指针。那为什么需要智能指针呢?
因为在C++有了异常之后,执行流并不像以前那样有迹可循,可能因为异常的抛出和捕获而导致执行流一连跳跃了好几个函数栈。这就可能导致一些在堆区申请的数据没能及时销毁回收,内存泄露,导致内存碎片化。
而智能指针就是为了防止异常等导致的内存泄露
内存泄露是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄露并不是指内存在物理上的消失,而是应用程序在分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费
长期运行的程序出现内存泄露,会导致最后没有足够的内存存放数据等,导致响应越来越慢,最终卡死
要介绍智能指针,就要先介绍RAII思想
。智能指针和lockguard都是RAII思想的产物
RAII(Resource Acquisition Is Initialization)是一种
利用对象生命周期
来控制程序资源(如内存,文件句柄,网络连接,互斥量等)的简单技术
核心有两点:
在对象构造时获取资源
,接着控制对资源的访问使之在对象的生命周期内始终保持有效。在对象析构的时候释放资源
,不用担心因为意外的跳出函数栈或作用域而没有释放资源智能指针本质是一个
类模板
,可以接收任意类型的指针。
智能指针的核心也就是RAII,上述我们讲到,当申请的堆空间,因为执行流跳跃而没有执行delete时,会出现内存泄露。但是使用智能指针,在构造时申请空间,在析构时释放空间,即使执行流跳转,局部对象出了作用域就会自动调用析构函数,这样就可以解决内存泄露的问题。
下面是智能指针的基本框架,也是RAII思想的体现。
接着,智能指针还需要能像指针那样使用,所以需要重载*
和->
这两个运算符。*运算符
是获取指针指向的内容,->运算符
是获取原生指针
这就是智能指针的两个要点:
但智能指针的使用还面临许多困难
智能指针面临的第一个困难就是——拷贝构造
我们直接使用上述智能指针展示以下场景
程序崩溃的原因是,同一块资源重复释放了。
因为拷贝构造属于默认成员函数,我们不写,编译器会自动生成,而生成的是浅拷贝,逻辑现象如下:
同时,如果我们自己编写深拷贝的拷贝构造也是不行的,因为原生指针是可以不同指针指向同一块资源
的。深拷贝与指针的效果相冲突。
为了解决无法指向同一资源这一问题,出现了三种智能指针。接下来,我们一一讲解
auto_ptr是第一个尝试解决这一问题的,但也并没有成功解决
auto_ptr采取的措施是管理权转移
,即如果发生拷贝构造,那么就将原先的资源转移给新的智能指针管理,原先的智能指针置空
具体实现即在原先SmartPtr的基础上编写一个拷贝构造和赋值重载
效果如下:
但这样并没有解决问题,只是程序不会崩溃而已。
而且原先的智能指针会被置空
,如果使用原先的空的智能指针就会导致程序崩溃,这带来的伤害还是很大的,所以很多公司在代码规范中都严厉禁止使用auto_ptr
unique_ptr正如其名,独一无二的指针,他解决问题的方式是不允许拷贝构造,直接将拷贝构造和赋值重载禁用
在C++11前,要禁用一个默认成员函数,首先我们要定义,不然编译器会默认生成,其次将其设置为私有,这样外部就不能调用该成员函数了
实现如下:
效果如下:
而在C++11中,直接提供了禁止生成默认成员函数的语法,就是=delete。具体实现如下:
unique_ptr虽然也没有解决拷贝的问题,但是因为禁止了拷贝,所以较为安全,
可以运用在不需要拷贝的场景
shared_ptr是C++11从boost库中吸收的智能指针,很好的解决了多个指针指向同一块资源的释放问题,
保证资源只有一次释放
shared_ptr采用引用计数
的方式,类内有成员变量记录指向当前资源的智能指针数量,当一个智能指针调用析构时,先将计数-1
,如果计数减为0,代表当前没有智能指针指向,就可以释放资源。如此就防止了资源的重复释放
所以shared_ptr的实现需要在SmartPtr的基础上,添加引用计数
引用计数
指向同一资源的对象需要有相同的引用计数,单使用int,是每个对象单独的引用计数;static 是所有对象共享一个引用计数;这两种都不对。使用指针申请堆空间可以满足需求
如图
基本框架如下:
然后是拷贝构造
和赋值重载
拷贝构造需要获得资源的同时,将引用计数+1
赋值重载较为复杂,需要分为三种情况:
原先资源的引用计数-1
并且判断是否需要释放原先资源
更改指向的资源
,并且该资源引用计数+1
效果如下:
为了保证线程安全,在引用计数的+1和-1时需要保证原子性,所以shared_ptr内部还应带有互斥量。同时我们还可以封装引用计数的+1和-1操作
template<class T>
class SharedPtr
{
public:
//构造函数初始化
SharedPtr(T*ptr)
:_ptr(ptr)
, _pcount(new int(1))//引用计数初始化为1
,_pmtx(new mutex)
{}
//拷贝构造
SharedPtr(SharedPtr&sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
//引用计数+1
addCount();
}
//引用计数+1
void addCount()
{
_pmtx->lock();
(*_pcount)++;
_pmtx->unlock();
}
//引用计数-1
void subCount()
{
_pmtx->lock();
//flag为1时,代表释放资源,同时释放锁
int flag = 0;
if (--(*_pcount) == 0)
{
//释放资源
cout << "delete " << _ptr << endl;
delete _ptr;
delete _pcount;
flag = 1;
}
_pmtx->unlock();
//锁要先解锁再释放
if (flag)
delete _pmtx;
}
//赋值重载
SharedPtr&operator = (SharedPtr&sp)
{
//指向不同资源时才做处理
if (_ptr != sp._ptr)
{
//旧资源引用计数-1
subCount();
//更改指向资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
//新资源引用计数+1
addCount();
}
return *this;
}
//析构函数释放资源
~SharedPtr()
{
subCount();
}
//*号重载
T&operator*()
{
return *_ptr;
}
//->重载
T*operator->()
{
return _ptr;
}
private:
int*_pcount;//引用计数
T*_ptr;//指针
mutex* _pmtx;//互斥量
};
虽然shared_ptr解决了多数问题,但是仍存在问题——循环引用
如果我们在双向链表中使用shared_ptr
我们发现,没有析构函数的打印信息,链表并没有释放。这是为什么呢?
因为ListNode内部使用了shared_ptr,所以当n1->_next=n2时,n2的引用计数会+1
。同理n2->_prev=n1也会让n1的引用计数+1。
这样就会导致,在析构时,比如n1先析构,引用计数-1,但因为n2->_prev还指向n1,导致n1引用计数为1,而没有析构
。
然后n2再析构,但还是会因为n1->_next,导致引用计数为1,没有析构。
这就是shared_ptr的循环引用问题。
为了解决循环引用问题,weak_ptr诞生。
weak_ptr有同shared_ptr一样的原生指针,但是不会增加引用计数。
补充一个shared_ptr的函数 get() 获取原生指针
简单实现如下:
然后在双向链表中,使用weak_ptr就不会增加计数了
weak_ptr平常使用不多,主要就是设计解决shared_ptr的循环引用问题
以上几个智能指针在C++库中都有
库中的shared_ptr还有一个D的参数,这就是定制删除器
当智能指针管理的是一个类型的数组时,我们应该使用delete [],而不是delete。所以为了实现不同类型指针,不同的释放方式,通过提供仿函数,完成自定义的释放动作。
D接收可调用对象:函数指针,仿函数,lambda表达式
也可以使用lambda表达式
模拟实现也比较简单,添加一个function包装器即可
感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。