目录
- 1. RAII与智能指针
- 2. C++库中的智能指针
- 2.1 智能指针auto_ptr
- 2.2 智能指针unique_ptr
- 2.3 智能指针shared_ptr
- 3. shared_ptr的循环引用
- 4. 智能指针的定值删除器
1. RAII与智能指针
上一篇文章学习了异常相关的知识,其中遗留了一个异常安全相关的问题。那就是异常的抛出会打乱执行流,可能使得动态开辟的资源无法被正常释放导致内存泄漏。而之前我们是通过异常再抛出的方式去解决这一问题的,可是,此种方式会使得代码的可读性极差。下面就来学习一种更好的也是现今一般会使用的解决异常安全的方式,智能指针。
在正式学习智能指针之前,先来了解一个概念RAII(Resource Acquisition Is Initializatio)
,RAII是C++中的一种编程设计思路,直译而来是资源获得后立即初始化。而实际上是指将资源交给一个对象去帮忙管理,利用对象的生命周期来管理资源,智能指针就是RAII思想设计而得一个产物。另外的应用场景,还有,打开文件与关闭文件,打开文件的返回值一般都是指针。
智能指针的特性与功能:
- 1. 智能指针会将资源管理起来,利用本身对象的生命周期在析构时释放资源,防止了资源的内存泄漏
- 2. 智能指针支持像指针一样的操作,诸如,
解引用*
,箭头->
- 3. 智能指针的拷贝不会进行深拷贝与迭代器类似,虽然其能对资源进行管理与操作,但与数据结构的存储不一样,数据结构中所存储的资源是自己的,而智能指针的资源是代为持有,多个智能指针是共享一份资源的(一般为引用计数)
//智能指针管理资源的逻辑与支持指针操作
template<class T>
class SmartPtr
{
public://管理资源SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}//像指针一样的操作:*, ->T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
2. C++库中的智能指针
2.1 智能指针auto_ptr
- 历史上的一个智能指针auto_ptr:
在C++98标准时,就已经有了C++中历史上的第一个智能指针auto_ptr
。其除了具备智能指针管理资源与指针操作的功能外,对智能指针的拷贝也做了相关的实现,其设计思路为拷贝后,将指针转移,将原有指针悬空。此种方法多有漏洞,大部分公司都禁止其的使用,可以说是C++语言历史上的一个语法污点。可能是受此影响,C++98标准后,一些C++标准委员会库工作组成员合理制作了一个名为boost的准标准库,其会将一个些新的语法点进行先探索与尝试实现,C++标准库后续的很多语法都是从boost库中吸收而来。
- auto_ptr的拷贝后指针悬空与简单实现模拟:
//模拟实现
template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr):_ptr(ptr){}//指针悬空auto_ptr(const auto_ptr<T>& p){_ptr = p._ptr;p._ptr = nullptr;}~auto_ptr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
2.2 智能指针unique_ptr
因为C++98中auto_ptr的缺陷,boost库中又尝试创建实现了新的智能指针,如shared_ptr(配合new)/shared_array(配合new[])
共享指针、scoped_ptr/scoped_arrary
守卫指针、weak_ptr
弱指针、instrusive_ptr。其中,shared_ptr
,scoped_ptr
与weak_ptr
都后续被纳入标准库。后续出现的这些指针都是旨在采用不同的方式去处理智能指针拷贝的问题。boost库中的scoped_ptr
就是现在C++标准库中的unique_ptr
。unique_ptr
解决拷贝问题的方式是,直接禁止本身进行拷贝操作,其原理为禁止拷贝构造与赋值重载的生成,C++11前的实现方法与C++11后的实现方法不同。
所有智能指针都包含在<memory>头文件中,boost库中将boost作为命名空间。
//unique_ptr简单模拟实现
template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){delete _ptr;}unique_ptr(const unique_ptr<T>& p) = delete;//拷贝构造unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;//赋值T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
2.3 智能指针shared_ptr
与unique_ptr
不同shared_ptr
支持拷贝操作,其底层是以引用计数的方式来支持拷贝构造与赋值操作的实现的。但选取怎样一个变量当作为引用计数的载体是一个值得思考的问题,智能指针的引用计数是指当前有多少个智能指针指向同一份资源。
- 选取普通的成员变量显然是不可行的,其无法保证拷贝时引用计数的共享性。
- 那么,属于整个类的静态成员变量呢?初步考量这好像是一个可行的方案,但这个方法其实还是有漏洞,当同一类型的
shared_ptr
指向不同的资源时,静态成员变量就无法解决了。
- C++标准库中给出的方法是,动态开辟new出一个变量,让其存储引用计数,这样指向不同资源的
shared_ptr
就不会互相印象,当引用计数归零时,对资源进行释放。
shared_ptr的简单模拟实现:
template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr = nullptr)//构造参数赋予缺省值,充当默认构造:_ptr(ptr){_pcount = new int(1);}void release(){if (--(*_pcount) == 0)//引用计数为0时,释放资源{delete _ptr;delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& p){_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& p){//不能自己给自己赋值//if(*this != p)//指向同一份资源的智能指针赋值,特殊处理if (_ptr != p._ptr){release();_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}return *this;}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const//指针指向的对象不能被改变{return _ptr;}private:T* _ptr;int* _pcount;
};
3. shared_ptr的循环引用
shared_ptr
在多种智能指针中,综合而论已经是最优秀的智能指针了,可时它真的就完全不会造成内存泄漏的问题了吗,我们来看下面这个场景。
自定义一个双向链表的节点,链表的每个节点都是动态开辟而出的,这里我们采用智能指针的方式去定义与管理。但当程序执行结束时,两个链表节点的资源并没有被释放。
当程序运行结束,node1、node2两个shared_ptr智能指针销毁之后。还有两个指向节点资源的智能指针_next
与_prev
。这就使得指向节点资源的智能指针其引用计数没有归0,所指向的资源也就无法释放。被智能指针管理的资源想要被释放,其引用计数就需要归0,想要引用计数归0,那么,所有指向该资源的智能指针都必须要销毁。但在这一过程中,会出现下图的逻辑闭环,导致节点1,节点2互相指向无法释放的逻辑闭环,造成循环引用,内存泄漏。
循环引用的解决方法weak_ptr:
为了解决上述shared_ptr的循环引用导致内存泄漏的问题,C++库中设计了weak_ptr
这样一个智能指针,其的种种普通特性都与shared_ptr智能指针相同,但特殊的是,使用它指向shared_ptr管理的资源,shared_ptr的引用计数不增加。weak_ptr只做链接功能,因为weak_ptr与shared_ptr不是一个类型的智能指针,weak_ptr想要从shared_ptr获取资源只有两个方式,一是被声明为友元,二是为shared_ptr添加get接口,get接口必须使用const修饰this指针。
template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}~weak_ptr(){}weak_ptr(const shared_ptr<T>& p){_ptr = p.get();}weak_ptr<T>& operator=(const shared_ptr<T>& p){_ptr = p.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
在上面weak_ptr智能指针的模拟实现中,没有为其添加引用计数。但在C++标准库中weak_ptr其实也是有引用计数的。只不过它的引用计数不参与空间的释放,weak_ptr的引用计数更像是一种监视,其的存在是为了防止weak_ptr去释放引用计数为0已经被释放过的空间。
4. 智能指针的定值删除器
上面所有关于智能指针的学习,对于shared_ptr智能指针地模拟实现,都是基于使用智能指针对单个动态开辟new出的对象做管理的情况。但当需要使用智能指针管理多个对象,或是管理非动态开辟的资源(文件指针)时,就无法去正确地释放资源了。
boost库中对于管理多个对象的资源创造了专门与之相对应的shared_array与scoped_array。但在C++标准库中,却没有采用这种方式,而是设计了一种定值删除器的方法来控制对资源的删除方式,使用方式如下:
//方法1:C++中特化模板,专门用于释放new[]的资源
shared_ptr<ListNode[]> p1(new ListNode[10]);//方法2:定值删除器,构造时传入以仿函数对象形式传入对应的资源释放方法
template<class T>
struct DeleteArray
{void operator()(T* ptr){delete[] ptr;}
};//仿函数、lambda表达式、函数指针皆可
shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
C++标准库中的调用接口:
- shared_ptr中定值删除器的模拟实现
namespace zyc
{template<class T>class shared_ptr{public://定值删除器function<void(T*)> _del = [](T* ptr) { delete ptr; };//delete普通new对象的缺省处理方法template<class D>shared_ptr(T* ptr, D del):_del(del){}shared_ptr(T* ptr = nullptr):_ptr(ptr){_pcount = new int(1);}void release(){if (--(*_pcount) == 0){_del(_ptr);//控制释放方式delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& p){_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& p){if (_ptr != p._ptr){release();_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}return *this;}private:T* _ptr;int* _pcount;};
}
含有定值删除器的模板构造中,其中定值包装器的类型是独属于此构造函数的模板参数类型,而不是整个类。因此,想要通过成员变量的方式让析构函数拿到这一仿函数对象,就需要采用定义包装器对象的方式来实现(释放资源的仿函数其参数与返回值类型是确定的)。
- 内存泄漏与资源泄漏:
内存泄漏是指动态开辟(malloc/realloc/new)出的空间已经不再使用,可是因为疏忽(忘记free/delete)/错误(循环引用)的原因没有去释放。而资源泄漏是指申请的资源(文件描述符,管道等)在使用完成忘记释放,资源描述符是有限的。在程序长期运行的环境下,内存泄漏可能会导致程序直接崩溃,而资源泄漏可能就会导致出现无法再打开文件等问题。
一般出现上述问题,在不同环境下都有内存泄漏的检测工具可以帮助我们发现问题,但再好的检测工具都不如我们在编写代码时多加注意,提高代码的规范性。每到必要时就去使用智能指针管理相关资源,如此就能预防避免几乎所有的资源泄漏问题。再好的事后检测手段,都不如事前做好预防。
使用cout打印char类型指针变量时,需要进行(void)强制类型转换,因为cout会默认char*类型为打印字符串。