C++智能指针简介

1.智能指针的由来

C++中,动态内存的管理是通过一对运算符来完成的,new用于申请内存空间,调用对象构造函数初始化对象并返回指向该对象的指针。delete接收一个动态对象的指针,调用对象的析构函数销毁对象,释放与之关联的内存空间。动态内存的管理在实际操作中并非易事,因为确保在正确的时间释放内存是极其困难的,有时往往会忘记释放内存而产生内存泄露;有时在上游指针引用内存的情况下释放了内存,就会产生非法的野指针(悬挂指针)。

为了更容易且更安全的管理动态内存,C++推出了智能指针(smart pointer)类型来管理动态对象。智能指针存储指向动态对象的指针,用于动态对象生存周期的控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露[1]^{[1]}[1]。

对动态内存的管理,可以引申为对系统资源的管理,但是C++程序中动态内存只是最常使用的一种资源,其他常见的资源还包括文件描述符(file descriptor)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets等,这些资源事实上都可以使用智能指针来管理。

2.智能指针的基本思想

智能指针的基本思想是以栈对象管理资源[2]^{[2]}[2]。考察如下示例:

void remodel(std::string & str)
{
    std::string* ps = new std::string(str);
    ...
    if (weird_thing())
    {
        throw exception();
    }
    str = *ps; 
    delete ps;
    return;
}

如果在函数remodel中出现异常,语句delete ps没有被执行,那么将会导致ps指向的string的堆对象残留在内存中,导致内存泄露。如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,往往会发现内存泄露时有发生,对于程序而言,这无疑是一场灾难!这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),函数体内的局部变量都将自动从栈内存中删除,因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果ps是一个局部的类对象,它指向堆对象,则可以在ps生命周期结束时,让它的析构函数释放它指向的堆对象[3]^{[3]}[3]。

通俗来讲, 智能指针就是模拟指针动作的类。所有的智能指针都会重载->和*操作符。智能指针的主要作用就是用栈智能指针离开作用域自动销毁时调用析构函数来释放资源。当然,智能指针还不止这些,还包括复制时可以修改源对象等。智能指针根据需求不同,设计也不同(写时复制,赋值即释放对象拥有权限、引用计数、控制权转移等)。

3.智能指针的引用计数

什么是引用计数? 智能指针有时需要将其管理的对象的所有权转移给其它的智能指针,使得多个智能指针管理同一个对象,比如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,则删除对象),并增加右操作数所指对象的引用计数。

4.智能指针的实现模板

智能指针管理对象,本质上是以栈对象来管理堆对象,在《Effective C++》的条款13中称之为资源获取即初始化(RAII,Resource Acquisition Is Initialization),也就是说我们在获得一笔资源后,尽量以独立的一条语句将资源拿来初始化某个资源管理对象。有时候获得的资源被拿来赋值(而非初始化)某个资源管理对象,但不论哪一种做法,获得一笔资源后应该立即放进资源管理对象中。

智能指针就是一种资源管理对象,提供的功能主要有如下几种: (1)以指针的行为方式访问所管理的对象,需要重载指针->操作符; (2)解引用(Dereferencing),获取所管理的对象,需要重载解引用*操作符; (3)智能指针在其声明周期结束时自动销毁其管理的对象; (4)引用计数、写时复制、赋值即释放对象拥有权限、控制权限转移。

第4条是可选功能,拥有四条中不同的功能对应着不同类型的智能指针,比如C++11在STL中引入的shared_ptr就实现了引用计数的功能,已经被C++11摒弃的auto_ptr[4]^{[4]}[4]实现了赋值即释放对象拥有权限,C++11引入的unique_ptr则实现了控制权限的转移功能。

根据智能指针的功能,通常用类模板实现如下:

template <class T> class SmartPointer
{  
private:  
    T *_ptr;  
public:  
    SmartPointer(T *p) : _ptr(p)  //构造函数  
    {  
    }  
    T& operator *()        //重载*操作符  
    {  
        return *_ptr;  
    }  
    T* operator ->()       //重载->操作符  
    {  
        return _ptr;  
    }  
    ~SmartPointer()        //析构函数  
    {  
        delete _ptr;  
    }  
};  

参考文献

[1]Stanley B.Lippman著,王刚,杨巨峰译.C++ Primer(第五版).2013:400-422 [2]Scott Meyers著,侯捷译.Effective C++中文版(第三版).2011:61-77 [3]C++智能指针简单剖析 [4]shared_ptr基于引用计数智能指针实现

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券