C++——智能指针
1. 引例
在上一篇文章的最后,我们提到对于多次new的问题,为了最完善的进行异常检查并避免内存泄漏,最好的解决方案是C++11引入的智能指针。
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;
};class A
{
public:int a;double b;string c;A(int a1 = 1, double b1 = 1.1, string c1 = ""):a(a1),b(b1),c(c1){}~A(){cout << "~A()" << endl;}
};double Div(int a, int b)
{int ret = 0;if (b == 0) throw "Devision by zero";else ret = double(a) / double(b);return ret;
}
void fun()
{//A* a1 = new A;//A* a2 = new A;//对于连续的new,可能在第二个new的时候抛异常导致第一个new的资源泄露//方案1:为解决这个问题需要为第二个new也使用try-catch捕捉异常,释放array1后再抛出异常A* a1;A* a2;try{a1 = new A;try{a2 = new A;}catch (...){// 如果a2分配失败,清理a1delete a1;throw;}}catch (...){// a1分配失败throw;}//方案2:更好地解决方法是智能指针//C++库智能指针//unique_ptr<A> a1 = new A;//unique_ptr<A> a2 = new A;//自己对指针封装//SmartPtr<A> p1=(new A);//SmartPtr<A> p2=(new A);
}
智能指针为指针包装了一个类的壳子,这样指针就变为了一个局部对象,那么在函数栈帧销毁时自动调用析构函数,从而完成资源的自动释放。这就是RAII,资源获得即初始化,利用对象的生命周期来控制资源的获取与释放。在对象的构造时获取资源,在对象生命周期内资源始终有效并可以借助对象访问,在对象析构时释放资源。
2. 智能指针介绍
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;
};
SmartPtr<A> sp1(new A); //函数解释,sp1局部对象生命周期结束,自动调用析构函数释放资源//SmartPtr<A> sp2(sp1); //error //拷贝构造下,sp1和sp2指向同一块资源,但是在函数解释后二者都调用析构释放资源,导致对同一块资源释放两次而出错
对于我们写的最简单的一个指针包装来说,对其进行拷贝构造得到两个指向同一个对象的智能指针,这似乎没有什么问题,毕竟多个指针指向同一块内存并不是什么罕见的事情。但是问题发生在析构释放空间,因为对象的销毁会自动调用析构函数,所以这两个智能指针都会被析构,使得同一块资源被释放两次而出错。
可见拷贝是智能指针面临的难题,为此C++中各种智能指针用自己的方法来应付拷贝的问题。
2.1 auto_ptr
auto_ptr是C++98中使用的智能指针,它对于拷贝构造的处理很不安全,采取拷贝时管理权转移,被拷贝对象悬空的方案实现拷贝构造。即拷贝构造后资源全部赋值给新的智能指针,而原智能指针被置为空,这就导致极易发生访问空指针行为。
//auto_ptr C++98版本的遗老auto_ptr<A> ap1(new A);auto_ptr<A> ap2(ap1);//拷贝时管理权转移,被拷贝对象悬空 //ap1->a++; //error//虽然auto_ptr提供了拷贝的功能,但是这个拷贝实际上是将资源转移了,ap1拷贝给ap2导致ap1的指针被置为空,通过ap1访问会导致空指针异常
2.1.1 模拟实现
template <class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}//管理权转移,原指针悬空auto_ptr(auto_ptr<T>& p):_ptr(p){p._ptr = nullptr;}//释放原资源,获得新的指针的管理权,原指针悬空auto_ptr<T>& operator=(auto_ptr<T>& p){if (this != &p){if (_ptr) delete _ptr;_ptr = p._ptr;p._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
2.2 unique_ptr
unique_ptr是C++11中的智能指针,它是boost库中的scoped_ptr、scoped_array的引入。boost库是C++的第三方拓展库,诞生于C++11之前,这些库旨在扩展C++标准库的功能,在C++发展中有着很重要的地位。
对于拷贝构造unique_ptr采取直接禁止的方式,不允许拷贝构造的发生。
//在C++11前的boost库中的智能指针//scoped_ptr,scoped_array——禁止拷贝,后被引入C++11,更名为unique_ptrunique_ptr<A> up1(new A);//unique_ptr<A> up2(up1);//error unique_ptr不支持拷贝
2.2.1 定制删除器
unique_ptr对象的析构函数底层就是简单的调用delete,对包装的指针试图进行delete释放。但是在一些场景下(如new[]、管理文件资源)释放资源并不能通过delete,这时就需要自己定制删除器,即为模板实例化传递一个仿函数。
给出两个适配于new[]和文件资源关闭的删除器的仿函数类,后文删除器均使用这两个例子。
template <class T>
class DeleteArray {
public:void operator()(T* ptr){delete[] ptr;}
};class DeleteFile {
public:void operator()(FILE* ptr){cout << "fclose" << endl;fclose(ptr);}
};
unique_ptr具有模板template <class T, class D = default_delete<T>> class unique_ptr;,其中模板参数D就是用于接收删除器。另外,unique_ptr模板还有对[]的特化版本template <class T, class D = std::default_delete<T[]>> class unique_ptr<T[], D>;,可以对new[]默认使用delete[]来释放。
unique_ptr<A, DeleteArray<A>> up1(new A[5]);unique_ptr<FILE, DeleteFile> up2(fopen("log.txt","w"));//对[]的特化版本template <class T, class D = std::default_delete<T[]>> class unique_ptr<T[], D>;unique_ptr<A[]> up3(new A[5]);
2.2.2 模拟实现
对于拷贝构造和赋值重载使用关键字delete禁止编译器自动生成。
template <class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}//禁止拷贝unique_ptr(const unique_ptr<T>& p) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;~unique_ptr(){if (_ptr){delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
2.3 shared_ptr
shared_ptr是C++11中的智能指针,它是boost库中的shared_ptr、shared_array的引入。它是通过引用计数的方式支持拷贝,如果指针需要拷贝一般就会使用shared_ptr。
//shared_ptr/shared_array——通过引用计数的方式支持拷贝,后被引入C++11,仍命名shared_ptrshared_ptr<A> shp1(new A);shared_ptr<A> shp2(shp1);//shared_ptr支持拷贝cout << shp1.use_count() << endl;//查看引用计数
引用计数实际上就是记录指向当前资源的指针数目,每当有额外的一个shared_ptr智能指针指向这个资源,所有管理同一份资源的指针的引用计数都会加一。通过引入引用计数,智能指针就知道除了自己是否还有其他智能指针在共同管理这一份资源。
如果有其他智能指针也指向这块空间(计数不为1),此时智能指针析构并不会释放资源,仅仅析构智能指针对象。当智能指针析构且计数为1的时候,说明自己脱钩后就没有对象再管理这部分资源了,于是才会释放资源空间。
2.3.1 定制删除器
对于shared_ptr来说,智能指针同样支持定制删除器。unique_ptr的定制删除器作为函数模板的参数传递,而shared_ptr和unique_ptr有所不同,它是将定制删除器作为一个对象,通过构造函数的参数传递。
//shared_ptr定制删除器需要作为一个对象,在构造函数的参数中传递shared_ptr<A> sp1(new A[5], DeleteArray<A>());shared_ptr<FILE> sp2(fopen("log.txt", "w"), DeleteFile());//shared_ptr对[]也有特化shared_ptr<A[]> sp3(new A[5]);
2.3.2 make_shared
make_shared是一个可变参数函数模板,用于接收参数来new一个对象,然后返回这个new出来的对象的智能指针。和emplace类似,如果要new一个对象T,那么就可以将构造所需的参数作为可变参数包传递参数,资源申请后返回其智能指针。
make_shared的优势在于内存管理,它可以将引用计数内存和对象的内存紧邻,减少内存碎片。
2.3.3 模拟实现
对于shared_ptr有两个需要关注的点:
①shared_ptr需要引用计数来记录指向同一块资源的指针数量,这个引用计数应该以什么形式存在呢?
如果计数属于类的非静态成员,此时所有对象都有一个独立的计数。这会导致指向同一块资源的的对象之间的计数无法互通。当有一个指针获取或释放资源,无法让指向同一块资源的的对象的计数做出调整。
如果计数属于类的静态成员,此时所有这个类的对象都共享这一个计数,每当创建一个智能指针,无论它管理的资源是否和其他智能指针相同,他们都会自增,同样的每取消一个索引的计数都会自减。这会导致无法区分指向不同资源的计数,因为静态成员被共享,只有一个。
所以综上所述,引用计数应该位于堆上,且每一块资源都有自己的一个引用计数。
正是因为引用计数也需要堆空间这样的结构,所以对一个智能指针,它管理着指针指向的资源块和引用计数块。所以才需要make_shared来优化内存布局,把这两部分紧邻放置,减少内存碎片。
②shared_ptr删除器对象通过构造函数传递,这就说明删除器应该是shared_ptr的一个成员。删除器作为一个可调用对象,最理想的接收类型就是function包装器了。删除器的功能是对指针进行释放,所以其返回值应该都是void,参数只需要指针即可。
为删除器给一个默认值,即delete ptr的函数。
③shared_ptr不需要移动构造,因为移动构造是拷贝构造的上位。但是这里的拷贝构造实际上只是浅拷贝(拷贝指针和计数指针,并不申请新资源),移动构造相比拷贝构造并没有效率上的优势。
template <class T>class shared_ptr{typedef std::function<void(T* ptr)> D;public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}//定制删除器shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}shared_ptr(const shared_ptr<T>& p):_ptr(p._ptr),_pcount(p._pcount){++(*_pcount);}//不需要移动构造,因为移动构造是拷贝构造的上位//但是这里的拷贝构造实际上只是浅拷贝(拷贝指针和计数指针,并不申请新资源)void release(){if (--(*_pcount)==0){//delete _ptr;_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}shared_ptr<T>& operator=(const shared_ptr<T>& p){//避免管理相同资源的对象相互赋值if (p._ptr != _ptr){release();_ptr = p._ptr;_pcount = p._pcount;++(*_pcount);}return *this;}~shared_ptr(){release();}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;D _del = [](T* ptr) {delete ptr; };//删除器};
2.4 weak_ptr
weak_ptr也是C++11从boost库引入的。它不支持直接管理资源,而是用于配合shared_ptr解决其循环引用导致的内存泄漏问题。但是weak_ptr支持拷贝构造,同时也支持对share_ptr的拷贝构造。
//weak_ptr<ListNode> w1(new ListNode); //error//但是weak_ptr支持拷贝构造:weak_ptr (const weak_ptr& x) noexcept;//同样也支持对share_ptr的拷贝构造:template <class U> weak_ptr(const shared_ptr<U>&x) noexcept;
2.4.1 循环引用
class ListNode
{
public:int a;//ListNode* _next;//ListNode* _prev;//shared_ptr的构造函数由explicit修饰,不允许隐式类型转换,所以指针也需要share_ptr类型//循环引用shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};void Test4()
{//循环引用引发内存泄漏shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}
对于如上的这份代码,当运行起来后会发现端倪。
首先是两个结点的智能指针的引用计数变为了2。这并不意外,因为结点成员_next和_prev都是shared_ptr类型,它们指向了两个结点,所以使得引用计数变多了。
其次发现结点并没有被析构,发生内存泄漏了,接下来分析一下原因。
n1对象指向第一个结点,n2对象指向第二个结点。但当结点之间由_next和_prev成员互相指向之后,第一个结点就由n1对象和n2对象的_prev成员指向,引用计数为2。同理,第二个结点就由n2对象和n1对象的_next成员指向,引用计数为2。
在n1、n2生命周期结束后,n2、n1先后被析构(后定义先析构),但是n2析构仅仅是使得第二个结点的引用计数变为1,还有第一个结点的_next成员指向它,资源不会释放。同理,n1析构仅仅是使得第一个结点的引用计数变为1,此时第二个结点还没有释放,还有第二个结点的_prev成员指向它,资源不会释放。
所以尽管n1和n2对象都被析构了,但是两个节点由于他们之间相互指向,所以仍然没有被释放,造成了内存泄露。
这是weak_ptr就可以派上用场了,因为weak_ptr的特性之一就是,当使用weak_ptr对share_ptr拷贝后,不增加引用计数,同时支持对share_ptr的拷贝构造,这也就是为什么weak_ptr可以解决循环引用的问题。将节点成员更改为weak_ptr类型即可。
class ListNode
{
public:int a;//ListNode* _next;//ListNode* _prev;//shared_ptr的构造函数由explicit修饰,不允许隐式类型转换,所以指针也需要share_ptr类型//循环引用//shared_ptr<ListNode> _next;//shared_ptr<ListNode> _prev;weak_ptr<ListNode> _next;weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
2.4.2 模拟实现
weak_ptr亦可看到引用计数,只是不对其做修改,为了简化将其略去。剩余需要考虑对shared_ptr的拷贝的构造。
template <class T>class weak_ptr{public:weak_ptr(){}weak_ptr(const std::shared_ptr<T>& sp):_ptr(sp.get()){} weak_ptr<T>& operator=(const std::shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr = nullptr;//实际应该也存储引用计数,此处为简化略去};
2.4.3 其他成员函数
weak_ptr不提供访问资源的*和->解引用接口。
weak_ptr<ListNode> wp;shared_ptr<ListNode> sp(new ListNode);{shared_ptr<ListNode> n1(new ListNode);wp = n1;sp = wp.lock();cout << wp.use_count() << endl;//实质上lock()就是返回指向资源的shared_ptr,因为weak_ptr不提供引用计数//所以需要通过赋给新的shared_ptr来让引用计数自增从而避免资源被释放}if (!wp.expired()){//weak_ptr不提供访问资源的*和->解引用接口sp->a = 1;}
2.4.3.1 expire()
expire()函数用于检查weak_ptr指向的对象是否过期,当引用计数为零就说明过期了,证明该资源不可用,函数返回true。
2.4.3.2 lock()
lock()一般用于自己可能块过期,但是不希望过期时,调用lock()返回一个shared_ptr对象,可以将这个对象赋值给和weak_ptr生命周期相同的shared_ptr,从而帮助其脱离过期的风险。