1. 智能指针的由来
C++中的动态内存需要用户自己来维护,动态开辟的空间,在出函数作用域或者程序正常退出前必须释放掉,否则会造成内存泄露,有时我们已经非常谨慎了,然防不胜防,为了更容易且更安全的管理动态内存,C++推出了智能指针(smart pointer)类型来管理动态对象。智能指针存储指向动态对象的指针,用于动态对象生存周期的控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露
RAII(Resource Acquisition Is Initialization)
资源分配即初始化:定义一个类来封装资源的分配和释放,在构造函数完成资源的分配
和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
如果在函数中出现异常,语句delete ps没有被执行,那么将会导致ps指向的string的堆对象残留在内存中,导致内存泄露。如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,往往会发现内存泄露时有发生,对于程序而言,这无疑是一场灾难!这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),函数体内的局部变量都将自动从栈内存中删除,因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果ps是一个局部的类对象,它指向堆对象,则可以在ps生命周期结束时,让它的析构函数释放它指向的堆对象[3]。
通俗来讲, 智能指针就是模拟指针动作的类。所有的智能指针都会重载->和*操作符。智能指针的主要作用就是用栈智能指针离开作用域自动销毁时调用析构函数来释放资源。当然,智能指针还不止这些,还包括复制时可以修改源对象等。智能指针根据需求不同,设计也不同(写时复制,赋值即释放对象拥有权限、引用计数、控制权转移等)。
2.智能指针的引用计数
什么是引用计数?
智能指针有时需要将其管理的对象的所有权转移给其它的智能指针,使得多个智能指针管理同一个对象,比如C++ STL中的shared_ptr支持多个智能指针管理同一个对象。这个时候智能指针就需要知道其引用的对象总共有多少个智能指针在引用在它,也就是说智能指针所管理的对象总共有多少个所有者,我们称之为引用计数(Reference Counting),因为智能指针在准备释放所引用的对象时,如果有其他的智能指针同时在引用这个对象时,则不能释放,而只能将引用计数减一。
引用计数的目的?
引用计数,是资源管理的一种技巧和手段,智能指针使用了引用计数,STL中的string也同样使用了引用计数并配合“写时复制”来实现存储空间的优化。总的来说,使用引用计数有如下两个目的:
(1)节省内存,提高程序运行效率。如何很多对象拥有相同的数据实体,存储多个数据实体会造成内存空间浪费,所以最好做法是让多个对象共享同一个数据实体。
(2)记录引用对象的所有者数量,在引用计数为0时,让对象的最后一个拥有者释放对象。
其实,智能指针的引用计数类似于Java的垃圾回收机制:java的垃圾的判定很简单,如果一个对象没有引用所指,那么该对象为垃圾。系统就可以回收了。
智能指针实现引用计数的策略。
大多数C++类用三种方法之一来管理指针成员:
(1)不管指针成员。复制时只复制指针,不复制指针指向的对象实体。当其中一个指针把其指向的对象的空间释放后,其它指针都成了悬挂指针。这是一种极端做法。
(2)当复制的时候,即复制指针,也复制指针指向的对象。这样可能造成空间的浪费。因为指针指向的对象的复制不一定是必要的。
(3) 第三种就是一种折中的方式。利用一个辅助类来管理指针的复制。原来的类中有一个指针指向辅助类对象,辅助类的数据成员是一个计数器和一个指针(指向原来的对象)。
可见,第三种方法是优先选择的方法,智能指针实现引用计数的策略主要有两种:辅助类与句柄类。使用句柄类尚未研究,本文以辅助类为例,来研究实现智能指针的引用计数。利用辅助类来封装引用计数和指向对象的指针。如此做,指针指针,辅助类对象与被引用对象的关系如下图所示:
这里写图片描述
辅助类将引用计数与智能指针类指向的对象封装在一起,引用计数记录有多少个智能指针指向同一对象。每次创建智能指针时,初始化智能指针并将引用计数置为1;当智能指针q赋值给另一个智能指针r时,即r=q,拷贝构造函数拷贝智能指针并增加q指向的对象的引用计数,递减r原来指向的对象的引用计数。也就是说对一个智能指针进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数。
3.智能指针的模板实现
智能指针管理对象,本质上是以栈对象来管理堆对象,在《Effective C++》的条款13中称之为资源获取就是初始化(RAII,Resource Acquisition Is Initialization),也就是说我们在获得一笔资源后,尽量以独立的一条语句将资源拿来初始化某个资源管理对象。有时候获得的资源被拿来赋值(而非初始化)某个对管理对象,但不论哪一种做法,获得一笔资源后应该立即放进资源管理对象中。
智能指针就是一种资源管理对象,提供的功能主要有如下几种:
(1)以指针的行为方式访问所管理的对象,需要重载指针->操作符;
(2)解引用(Dereferencing),获取所管理的对象,需要重载解引用*操作符;
(3)智能指针在其声明周期结束时自动销毁其管理的对象;
(4)引用计数、写时复制、赋值即释放对象拥有权限、控制权限转移。
第4条是可选功能,拥有第四条中不同的功能对应着不同类型的智能指针,比如C++11在STL中引入的shared_ptr就实现了引用计数的功能,已经被C++11摒弃的auto_ptr[4]实现了赋值即释放对象拥有权限,C++11引入的unique_ptr则实现了控制权限的转移功能。
- 辅助类
template <class T> class Smart_ptr ;
template <class T>
class Refptr{
private:
friend class Smart_ptr<T>;
Refptr(T* ptr)
: p(ptr)
,ref(1)
{}
~Refptr()
{
delete p;
}
Refptr(const Refptr<T>& r);
Refptr& operator = (const Refptr<T>& r);
int ref;
T* p;
};
智能指针类
class Smart_ptr{ public: Smart_ptr(T* ptr = NULL) :_p(new Refptr<T>(ptr)) //构造辅助类对象来初始化指针 {} Smart_ptr(const Smart_ptr<T>& s) :_p(s._p) { ++s._p->ref; } Smart_ptr& operator =(const Smart_ptr<T>& s) { if (this != &s) { ++s._p->ref; if (--_p->ref == 0) delete _p; _p = s._p; } return *this; } T& operator *() const { return *(_p->p); } T& operator ->() const { return _p->p; } ~Smart_ptr() { if ( -- _p->ref == 0) delete _p; else cout<<"还有"<<_p->ref<<"个"<<"指针使用它"<<endl; } private: Refptr<T> *_p; };
测试
void Funtest()
{
Smart_ptr<int> p1(new int);
Smart_ptr<int> p2(p1);
Smart_ptr<int> p3;
p3 = p2;
}
int main()
{
Funtest();
return 0;
}
4.STL的另外两种指针
这里研究一下C++11引入的另外两种智能指针:unique_ptr与weak_ptr。C++准标准库Boost中的智能指针,比如boost::scoped_ptr、boost::shared_array、boost:: intrusive_ptr在这里不做研究,有兴趣的读者可以参考:C++ 智能指针详解。
4.1unique_ptr
unique_ptr是一种定义在< memory>中的智能指针(smart pointer)。它持有对对象的独有权——两个unique_ptr不能指向一个对象,即unique_ptr不共享它的所管理的对象。它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动 unique_ptr,即对资源管理权限可以实现转。这意味着,内存资源所有权可以将转移到另一个unique_ptr,并且原始 unique_ptr 不再拥有此资源。实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr,而当构造 unique_ptr 时,可使用 make_unique Helper 函数。
unique_ptr与原始指针一样有效,并可用于 STL 容器。将 unique_ptr 实例添加到 STL 容器很有效,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权,unique_ptr还可能没有对象,这种情况被称为empty。
4.2weak_ptr简介
weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->,因此取名为weak,表明其是功能较弱的智能指针。它的最大作用在于协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 boost::weak_ptr 只对 boost::shared_ptr 进行引用,而不改变其引用计数,当被观察的 boost::shared_ptr 失效后,相应的 boost::weak_ptr 也相应失效。
用法
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr管理的对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。总结来说,weak_ptr的基本用法总结如下:
weak_ptr<T> w; //创建空weak_ptr,可以指向类型为T的对象。
weak_ptr<T> w(sp); //与shared_ptr指向相同的对象,shared_ptr引用计数不变。T必须能转换为sp指向的类型。
w=p; //p可以是shared_ptr或weak_ptr,赋值后w与p共享对象。
w.reset(); //将w置空。
w.use_count(); //返回与w共享对象的shared_ptr的数量。
w.expired(); //若w.use_count()为0,返回true,否则返回false。
w.lock(); //如果expired()为true,返回一个空shared_ptr,否则返回非空shared_ptr。
现在要说的问题是,weak_ptr到底有什么作用呢?从上面那个例子看来,似乎没有任何作用。其实weak_ptr可用于打破循环引用。引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环引用的对象。一个简单的例子如下
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
using namespace boost;
struct ListNode
{
shared_ptr<ListNode > _prev;
shared_ptr<ListNode > _next;
//weak_ptr<ListNode > _prev;
//weak_ptr<ListNode > _next;
~ ListNode()
{
cout<<"~ListNode()" <<endl;
}
};
void Test ()
{
// 循环引用问题
shared_ptr <ListNode > p1( new ListNode ());
shared_ptr <ListNode > p2( new ListNode ());
cout <<"p1->Count:" << p1. use_count()<<endl ;
cout <<"p2->Count:" << p2. use_count()<<endl ;
// p1节点的_next指向 p2节点
p1->_next = p2;
// p2节点的_prev指向 p1节点
p2->_prev = p1;
cout <<"p1->Count:" << p1. use_count ()<<endl ;
cout <<"p2->Count:" << p2. use_count ()<<endl ;
}
一般来讲,解除这种循环引用有下面三种可行的方法:
(1)当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
(2)当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
(3)使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法,使用弱引用的智能指针std:weak_ptr来打破循环引用。
weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放。做法就是上面的代码注释的地方取消注释,取消Woman类或者Man类的任意一个即可,也可同时取消注释,全部换成弱引用weak_ptr。
另外很自然地一个问题是:既然weak_ptr不增加资源的引用计数,那么在使用weak_ptr对象的时候,资源被突然释放了怎么办呢?呵呵,答案是你根本不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()方法。
注意:shared_ptr实现了operator bool() const方法来判断一个管理的资源是否被释放。
如何选取智能指针
在掌握了上面提到的C++ STL中的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南。
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
(1.1)有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
(1.2)两个对象都包含指向第三个对象的指针;
(1.3)STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器中,只要不调用将一个unique_ptr复制或赋值给另一个的算法(如sort())。
模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。
参考原文