[文摘]Boost程序库完全开发指南:深入C++“准”标准库 之 内存管理
2010-10-25 21:05:51 作者:罗剑锋 来源:电子工业出版社
内存管理一直是令C++程序员最头疼的工作,C++继承了C那高效而又灵活的指针,使用起来稍微不小心就会导致内存泄漏(memory leak)、“野”指针(wild pointer)、访问越界(access denied)等问题。曾几何时,C++程序员曾经无限地向往Java、C#等语言的垃圾回收机制。虽然C++标准提供了智能指针std::auto_ptr,但并没有解决所有问题。
阅读完本章,你会了解到高效的内存管理方法,彻底忘记“栈”(Stack)、“堆”(Heap)等内存分配相关的术语,并且还会发现,Boost为C++提供的解决方案可能要比Java和C#等其他语言更好。
3.1 smart_ptr库概述
计算机系统中资源有很多种,内存是我们最常用到的,此外还有文件描述符、socket、操作系统handle、数据库连接等等,程序中申请这些资源后必须及时归还系统,否则就会产生难以预料的后果。
3.1.1 RAII机制
为了管理内存等资源,C++程序员通常采用RAII机制(资源获取即初始化,Resource Acquisition Is Initialization),在使用资源的类的构造函数中申请资源,然后使用,最后在析构函数中释放资源。
如果对象是用声明的方式在栈上创建的(一个局部对象),那么RAII机制会工作正常,当离开作用域时对象会自动销毁从而调用析构函数释放资源。但如果对象是用new操作符在堆上创建的,那么它的析构函数不会自动调用,程序员必须明确地用对应的delete操作符销毁它才能释放资源。这就存在着资源泄漏的隐患,因为这时没有任何对象对已经获取的资源负责,如果因某些意外导致程序未能执行delete语句,那么内存等资源就永久地丢失了。例如:
- int *p = new class_need_resource; //对象创建,获取资源
- .. //可能发生异常导致资源泄漏
- delete p; //析构释放资源
new、delete以及指针的不恰当运用是C++中造成资源获取/释放问题的根源,能否正确而明智地运用delete是区分C++新手与熟手的关键所在 。但很多人——即使是熟练的C++程序员,也经常会忘记调用delete。
3.1.2 智能指针
智能指针(smart pointer)是C++群体中热门的议题,围绕它有很多有价值的讨论和结论。它实践了推荐书目[1]中的代理模式,代理了原始“裸”指针的行为,为它添加了更多更有用的特性。
C++引入异常机制后,智能指针由一种技巧升级为一种非常重要的技术,因为如果没有智能指针,程序员必须保证new对象能在正确的时机delete,四处编写异常捕获代码以释放资源,而智能指针则可以在退出作用域时——不管是正常流程离开或是因异常离开——总调用delete来析构在堆上动态分配的对象。
存在很多种智能指针,其中最有名的应该是C++98标准中的“自动指针”std::auto_ptr,它部分地解决了获取资源自动释放的问题,例如:
- int main()
- {
- auto_ptr< class_need_resource > p1(new class_need_resource);
- auto_ptr<demo_class> p2(factory.create());
- ...
- } //离开作用域,p1、p2自动析构从而释放内存等资源
auto_ptr的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但因为重载了operator*和opreator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时(离开函数main()或者发生异常),C++语言会保证auto_ptr对象销毁,调用auto_ptr的析构函数,进而使用delete操作符删除原始指针释放资源。
auto_ptr很好用,被包含在C++标准库中令它在世界范围内被广泛使用,使智能指针的思想、用法深入人心。但标准库并没有覆盖智能指针的全部领域,尤其是最重要的引用计数型智能指针。
boost.smart_ptr库是对C++98标准的一个绝佳补充。它提供了六种智能指针,包括scoped_ptr、scoped_array、shared_ptr、shared_array,weak_ptr和intrusive_ ptr,从各个方面来增强std::auto_ptr,而且是异常安全的。库中的两个类——shared_ptr和weak_ptr已被收入到C++新标准的TR1库中。
接下来的部分将详细介绍scoped_ptr、scoped_array、shared_ptr和shared_ array,简要介绍另两个组件weak_ptr和intrusive_ptr。它们都是很轻量级的对象,速度与原始指针相差无几,对于所指的类型T也仅有一个很小且很合理的要求:类型T的析构函数不能抛出异常。
这些智能指针都位于名字空间boost,为了使用smart_ptr组件,需要包含头文件<boost/smart_ptr.hpp>,即:
#include <boost/smart_ptr.hpp> using namespace boost;
3.2 scoped_ptr
scoped_ptr是一个很类似auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但scoped_ptr的所有权更加严格,不能转让,一旦scoped_ptr获取了对象的管理权,你就无法再从它那里取回来。
scoped_ptr拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让。
3.2.1 类摘要
scoped_ptr的类摘要如下:
- template<class T>
- class scoped_ptr { //noncopyable
- private:
- T * px;
- scoped_ptr(scoped_ptr const &);
- scoped_ptr & operator=(scoped_ptr const &);
- public:
- explicit scoped_ptr(T * p = 0);
- ~scoped_ptr();
- void reset(T * p = 0);
- T & operator*() const;
- T * operator->() const;
- T * get() const;
- operator unspecified-bool-type() const;
- void swap(scoped_ptr & b);
- };
3.2.2 操作函数
scoped_ptr的构造函数接受一个类型为T*的指针p,创建出一个scoped_ptr对象,并在内部保存指针参数p。p必须是一个new表达式动态分配的结果,或者是个空指针(0)。当scoped_ptr对象的生命期结束时,析构函数~scoped_ptr()会使用delete操作符自动销毁所保存的指针对象,从而正确地回收资源 。
scoped_ptr同时把拷贝构造函数和赋值操作符都声明为私有的,禁止对智能指针的复制操作(原理可参考4.1小节,95页noncopyable,),保证了被它管理的指针不能被转让所有权。
成员函数reset()的功能是重置scoped_ptr:它删除原来保存的指针,再保存新的指针值p。如果p是空指针,那么scoped_ptr将不持有任何指针。一般情况下reset()不应该被调用,因为它违背了scoped_ptr的本意——资源应该一直由scoped_ptr自己自动管理。
scoped_ptr用operator*()和operator->()重载了解引用操作符*和箭头操作符->,以模仿被代理的原始指针的行为,因此可以把scoped_ptr对象如同指针一样使用。如果scoped_ptr保存空指针,那么这两个操作的行为未定义。
scoped_ptr不支持比较操作,不能在两个scoped_ptr之间、scoped_ptr和原始指针或空指针之间进行相等或者不相等测试,我们也无法为它编写额外的比较函数,因为它已经将operator==和operator!=两个操作符重载都声明为私有的。但scoped_ptr提供了一个可以在bool语境(context)中自动转换成bool值(如if的条件表达式)的功能,用来测试scoped_ptr是否持有一个有效的指针(非空)。它可以代替与空指针的比较操作,而且写法更简单。
成员函数swap()可以交换两个scoped_ptr保存的原始指针。它是高效的操作,被用于实现reset()函数,也可以被boost::swap(4.5小节,113页)所利用。
最后是成员函数get(),它返回scoped_ptr内部保存的原始指针,可以用在某些要求必须是原始指针的场景(如底层的C接口)。但使用时必须小心,这将使原始指针脱离scoped_ptr的控制!不能对这个指针做delete操作,否则scoped_ptr析构时会对已经删除的指针再进行删除操作,发生未定义行为(通常是程序崩溃,这可能是最好的结果,因为它说明你的程序存在bug)。
3.2.3 用法
scoped_ptr的用法很简单:在原本使用指针变量接受new表达式结果的地方改成用scoped_ptr对象,然后去掉哪些多余的try/catch和delete操作就可以了。像这样:
- scoped_ptr<string> sp(new string("text"));
scoped_ptr是一种“智能指针”,因此其行为与普通指针基本相同,可以使用非常熟悉的*和->操作符:
- cout << *sp << endl; //取字符串的内容
- cout << sp->size() << endl; //取字符串的长度
但记住:不再需要delete操作,scoped_ptr会自动地帮助我们释放资源。如果我们对scoped_ptr执行delete会得到一个编译错误:因为scoped_ptr是一个行为类似指针的对象,而不是指针,对一个对象应用delete是不允许的。
scoped_ptr不允许拷贝、赋值,只能在scoped_ptr被声明的作用域内使用。除了*和->外scoped_ptr也没有定义其他的操作符(不能对scoped_ptr进行++或者--等指针算术操作)。与普通指针相比它只有很小的接口,因此使指针的使用更加安全,更容易使用同时更不容易被误用。下面的代码都是scoped_ptr的错误用法:
- sp++; //错误, scoped_ptr未定义递增操作符
- scoped_ptr<string> sp2 = sp; //错误, scoped_ptr不能拷贝构造
使用scoped_ptr会带来两个好处:一是使代码变得清晰简单,而简单意味着更少的错误;二是它并没有增加多余的操作,安全的同时保证了效率,可以获得与原始指针同样的速度。
示范scoped_ptr用法的另一段代码如下:
- #include <boost/smart_ptr.hpp>
- using namespace boost;
- struct posix_file //一个示范性质的文件类
- {
- posix_file(const char * file_name) //构造函数打开文件
- {cout << "open file:" << file_name << endl; }
- ~posix_file() //析构函数关闭文件
- {cout << "close file" << endl; }
- };
- int main()
- {
- scoped_ptr<int> p(new int); //一个int指针的scoped_ptr
- if (p) //在bool语境中测试指针是否有效
- {
- *p = 100; //可以像普通指针一样使用解引用操作符*
- cout << *p << endl;
- }
- p.reset(); //reset()置空scoped_ptr,仅仅是演示
- assert(p == 0); //p不持有任何指针
- if (!p) //在bool语境中测试,可以用!操作符
- { cout << "scoped_ptr == null" << endl; }
- //文件类的scoped_ptr,
- //将在离开作用域时自动析构,从而关闭文件释放资源
- scoped_ptr<posix_file> fp(new posix_file("/tmp/a.txt"));
- } //在这里发生scoped_ptr的析构,
- //p和fp管理的指针自动被删除
程序运行结果如下:
100 scoped_ptr == null open file:/tmp/a.txt close file
3.2.4 与auto_ptr的区别
scoped_ptr的用法与auto_ptr几乎一样,大多数情况下它可以与auto_ptr相互替换,它也可以从一个auto_ptr获得指针的管理权(同时auto_ptr失去管理权)。
scoped_ptr也具有auto_ptr同样的“缺陷”——不能用作容器的元素,但原因不同:auto_ptr是因为它的转移语义,而scoped_ptr则是因为不支持拷贝和赋值,不符合容器对元素类型的要求。
scoped_ptr与auto_ptr的根本性区别在于指针的所有权。auto_ptr特意被设计为指针的所有权是可转移的,可以在函数之间传递,同一时刻只能有一个auto_ptr管理指针。它的用意是好的,但转移语义太过于微妙,不熟悉auto_ptr特性的初学者很容易误用引发错误。而scoped_ptr把拷贝构造函数和赋值函数都声明为私有的,拒绝了指针所有权的转让——除了scoped_ptr自己,其他任何人都无权访问被管理的指针,从而保证了指针的绝对安全。
下面的代码清楚地演示了两者的区别:
- auto_ptr<int> ap(new int(10)); //一个int自动指针
- scoped_ptr<int> sp(ap); //从auto_ptr获得原始指针
- assert(ap.get() == 0); //原auto_ptr不再拥有指针
- ap.reset(new int(20)); //auto_ptr拥有新的指针
- cout << *ap << "," << *sp << endl;
- auto_ptr<int> ap2;
- ap2 = ap; //ap2从ap获得原始指针,发生所有权转移
- assert(ap.get() == 0); //ap不再拥有指针
- scoped_ptr<int> sp2; //另一个scoped_ptr
- sp2 = sp; //赋值操作,无法通过编译!!
如果代码编写者企图从一个scoped_ptr构造或赋值另一个scoped_ptr(代码的最后一行),那么编译器会报出一个错误,阻止他这么做,从而保护了你的代码,而且是在编译期。
比起auto_ptr,scoped_ptr更明确地表明了代码原始编写者的意图:只能在定义的作用域内使用,不可转让,这在代码后续的维护生命周期中很重要。
3.3 scoped_array
scoped_array很像scoped_ptr,它包装了new[]操作符(不是单纯的new)在堆上分配的动态数组,为动态数组提供了一个代理,保证可以正确地释放内存。
scoped_array弥补了标准库中没有指向数组的智能指针的缺憾。
3.3.1 类摘要
scoped_array的类摘要如下:
- template<class T> class scoped_array //noncopyable
- {
- public:
- explicit scoped_array(T * p = 0);
- ~scoped_array();
- void reset(T * p = 0);
- T & operator[](std::ptrdiff_t i) const;
- T * get() const;
- operator unspecified-bool-type() const;
- void swap(scoped_array & b);
- };
scoped_array的接口和功能几乎是与scoped_ptr是相同的(甚至还要少一些),主要特点如下:
- 构造函数接受的指针p必须是new[]的结果,而不能是new表达式的结果;
- 没有*、->操作符重载,因为scoped_array持有的不是一个普通指针;
- 析构函数使用delete[]释放资源,而不是delete;
- 提供operator[]操作符重载,可以像普通数组一样用下标访问元素;
- 没有begin()、end()等类似容器的迭代器操作函数。
3.3.2 用法
scoped_array与scoped_ptr源于相同的设计思想,故而用法非常相似:它只能在被声明的作用域内使用,不能拷贝、赋值。唯一不同的是scoped_array包装的是new[]产生的指针,并在析构时调用delete[],因为它管理的是动态数组,而不是单个动态对象。
通常scoped_array的创建方式是这样的:
- scoped_array<int> sa(new int[100]); //包装动态数组
scoped_array重载了operator[],因此它用起来就像是一个普通的数组,但因为它不提供指针运算,所以不能用“数组首地址+N”的方式访问数组元素:
- sa[0] = 10; //正确用法,使用operator[]
- *(sa + 1) = 20; //错误用法,不能通过编译
在使用重载的operator[]时要小心,scoped_array不提供数组索引的范围检查,如果使用了超过动态数组大小的索引或者是负数索引将引发未定义行为。
下面的代码进一步示范了scoped_array的用法:
- #include <boost/smart_ptr.hpp>
- using namespace boost;
- int main()
- {
- int *arr = new int[100]; //一个整数的动态数组
- scoped_array<int> sa(arr); //scoped_array对象代理原始动态数组
- fill_n(&sa[0],100, 5); //可以使用标准库算法赋值数据
- sa[10] = sa[20] + sa[30]; //用起来就像是个普通数组
- } //这里scoped_array被自动析构,
- //释放动态数组资源
3.3.3 使用建议
scoped_array没有给程序增加额外的负担,用起来很方便轻巧。它的速度与原始数组同样快,很适合那些习惯于用new操作符在堆上分配内存的程序员。但scoped_array的功能很有限,不能动态增长,也没有迭代器支持,不能搭配STL算法,仅有一个纯粹的“裸”数组接口。而且,我们应当尽量避免使用new[]操作符,它比new更可怕,是许多错误的来源,因为
- int *p = new int[10];
- delete p;
这样的代码完全可以通过编译,无论是编译器还是程序员都很难区分出new[]和new分配的空间,而错误地运用delete将导致资源异常。
在需要动态数组的情况下我们应该使用std::vector,它比scoped_array提供了更多的灵活性,而只付出了很小的代价。使用std::vector,之前的例子可以写成:
- vector<int> sa(100, 5);
- sa[10] = sa[20] + sa[30];
很明显,std::vector只用一条语句就完成了scoped_array三条语句的初始化和赋值工作。因为vector有丰富的成员函数来操纵数据,能够使代码更加简单明了,易于维护。
除非对性能有非常苛刻的要求,或者编译器不支持标准库(比如某些嵌入式操作系统),否则本书不推荐使用scoped_array,它只是为了与老式C风格代码兼容而使用的类,它的出现往往意味着你的代码中存在着隐患。
3.4 shared_ptr
shared_ptr是一个最像指针的“智能指针”,是boost.smart_ptr库中最有价值、最重要的组成部分,也是最有用的,Boost库的许多组件——甚至还包括其他一些领域的智能指针都使用了shared_ptr。抱歉,我实在想不出什么更恰当的词汇来形容它在软件开发中的重要性。再强调一遍,shared_ptr非常有价值、非常重要、非常有用。
shared_ptr与scoped_ptr一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针 ,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时才删除被包装的动态分配的对象。shared_ptr也可以安全地放到标准容器中,并弥补了auto_ptr因为转移语义而不能把指针作为STL容器元素的缺陷。
在C++历史上曾经出现过无数的引用计数型智能指针实现,但没有一个比得上boost::shared_ptr,在过去、现在和将来,它都是最好的(咳~~非本站观点-C++编程网)。
3.4.1 类摘要
shared_ptr要比同为智能指针的scoped_ptr复杂许多,它的类摘要如下:
- template<class T> class shared_ptr
- {
- public:
- typedef T element_type;
- shared_ptr();
- template<class Y> explicit shared_ptr(Y * p);
- template<class Y, class D> shared_ptr(Y * p, D d);
- ~shared_ptr();
- shared_ptr(shared_ptr const & r);
- template<class Y> explicit shared_ptr(std::auto_ptr<Y> & r);
- shared_ptr & operator=(shared_ptr const & r);
- template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r);
- template<class Y> shared_ptr & operator=(std::auto_ptr<Y> & r);
- void reset();
- template<class Y> void reset(Y * p);
- template<class Y, class D> void reset(Y * p, D d);
- T & operator*() const;
- T * operator->() const;
- T * get() const;
- bool unique() const;
- long use_count() const;
- operator unspecified-bool-type() const;
- void swap(shared_ptr & b);
- };
3.4.2 操作函数
shared_ptr与scoped_ptr同样是用于管理new动态分配对象的智能指针,因此功能上有很多相似之处:它们都重载了*和->操作符以模仿原始指针的行为,提供隐式bool类型转换以判断指针的有效性,get()可以得到原始指针,并且没有提供指针算术操作。例如:
- shared_ptr<int> spi(new int); //一个int的shared_ptr
- assert(spi); //在bool语境中隐式转换为bool值
- *spi = 253; //使用解引用操作符*
- shared_ptr<string> sps(new string("smart"));//一个string的shared_ptr
- assert(sps->size() == 5); //使用箭头操作符->
但shared_ptr的名字表明了它与scoped_ptr的主要不同:它是可以被安全共享的——shared_ptr是一个“全功能”的类,有着正常的拷贝、赋值语义,也可以进行shared_ptr间的比较,是“最智能”的智能指针。
shared_ptr有多种形式的构造函数,应用于各种可能的情形:
- 无参的shared_ptr()创建一个持有空指针的shared_ptr;
- shared_ptr(Y * p)获得指向类型T的指针p的管理权,同时引用计数置为1。这个构造函数要求Y类型必须能够转换为T类型;
- shared_ptr(shared_ptr const & r)从另外一个shared_ptr获得指针的管理权,同时引用计数加1,结果是两个shared_ptr共享一个指针的管理权;
- shared_ptr(std::auto_ptr<Y> & r)从一个auto_ptr获得指针的管理权,引用计数置为1,同时auto_ptr自动失去管理权;
- operator=赋值操作符可以从另外一个shared_ptr或auto_ptr获得指针的管理权,其行为同构造函数;
- shared_ptr(Y * p, D d)行为类似shared_ptr(Y * p),但使用参数d指定了析构时的定制删除器,而不是简单的delete。这部分将在3.4.5小节,75页详述。
shared_ptr的reset()函数的行为与scoped_ptr也不尽相同,它的作用是将引用计数减1,停止对指针的共享,除非引用计数为0,否则不会发生删除操作。带参数的reset()则类似相同形式的构造函数,原指针引用计数减1的同时改为管理另一个指针。
shared_ptr有两个专门的函数来检查引用计数。unique()在shared_ptr是指针的唯一所有者时返回true(这时shared_ptr的行为类似auto_ptr或scoped_ptr),use_count()返回当前指针的引用计数。要小心,use_count()应该仅仅用于测试或者调试,它不提供高效率的操作,而且有的时候可能是不可用的(极少数情形)。而unique()则是可靠的,任何时候都可用,而且比use_count()==1速度更快。
shared_ptr还支持比较运算,可以测试两个shared_ptr的相等或不相等,比较基于内部保存的指针,相当于a.get()==b.get()。shared_ptr还可以使用operator<比较大小,同样基于内部保存的指针,但不提供除operator<以外的比较操作符,这使得shared_ptr可以被用于标准关联容器(set和map):
- typedef shared_ptr<string> sp_t; //shared_ptr类型定义
- map<sp_t, int> m; //标准映射容器
- sp_t sp(new string("one")); //一个shared_ptr对象
- m[sp] = 111; //关联数组用法
在编写基于虚函数的多态代码时指针的类型转换很有用,比如把一个基类指针转型为一个子类指针或者反过来。但对于shared_ptr不能使用诸如static_cast<T*>(p.get())的形式,这将导致转型后的指针无法再被shared_ptr正确管理。为了支持这样的用法,shared_ptr提供了类似的转型函数static_pointer_cast<T>()、const_pointer_cast<T>()和dynamic_pointer_cast<T>(),它们与标准的转型操作符static_cast<T>、const_cast <T>和dynamic_cast<T>类似,但返回的是转型后的shared_ptr。
例如,下面的代码使用dynamic_pointer_cast把一个shared_ptr<std::exception>向下转型为一个shared_ptr<bad_exception>,然后又用static_pointer_cast重新转为shared_ptr<std::exception>:
- shared_ptr<std::exception> sp1(new bad_exception("error"));
- shared_ptr<bad_exception> sp2 =
- dynamic_pointer_cast<bad_exception>(sp1);
- shared_ptr<std::exception> sp3 =
- static_pointer_cast<std::exception>(sp2);
- assert(sp3 == sp1);
此外,shared_ptr还支持流输出操作符operator<<,输出内部的指针值,方便调试。
3.4.3 用法
shared_ptr的智能使其行为最接近原始指针,因此它比auto_ptr和scoped_ptr的应用范围更广。几乎是100%可以在任何new出现的地方接受new的动态分配结果,然后被任意使用,从而完全消灭delete的使用和内存泄漏,而它的用法与auto_ptr和scoped_ptr一样的简单。
shared_ptr也提供基本的线程安全保证,一个shared_ptr可以被多个线程安全读取,但其他的访问形式结果是未定义的。
示范shared_ptr基本用法的例子如下:
- shared_ptr<int> sp(new int(10)); //一个指向整数的shared_ptr
- assert(sp.unique()); //现在shared_ptr是指针的唯一持有者
- shared_ptr<int> sp2 = sp; //第二个shared_ptr,拷贝构造函数
- //两个shared_ptr相等,指向同一个对象,引用计数为2
- assert(sp == sp2 && sp.use_count() == 2);
- *sp2 = 100; //使用解引用操作符修改被指对象
- assert(*sp == 100); //另一个shared_ptr也同时被修改
- sp.reset(); //停止shared_ptr的使用
- assert(!sp); //sp不再持有任何指针(空指针)
第二个例子示范了shared_ptr较复杂的用法:
- class shared //一个拥有shared_ptr的类
- {
- private:
- shared_ptr<int> p; //shared_ptr成员变量
- public:
- shared(shared_ptr<int> p_):p(p_){}//构造函数初始化shared_ptr
- void print() //输出shared_ptr的引用计数和指向的值
- {
- cout << "count:" << p.use_count()
- << "v =" <<*p << endl;
- }
- };
- void print_func(shared_ptr<int> p) //使用shared_ptr作为函数参数
- {
- //同样输出shared_ptr的引用计数和指向的值
- cout << "count:" << p.use_count()
- << " v=" <<*p << endl;
- }
- int main()
- {
- shared_ptr<int> p(new int(100));
- shared s1(p), s2(p); //构造两个自定义类
- s1.print();
- s2.print();
- *p = 20; //修改shared_ptr所指的值
- print_func(p);
- s1.print();
- }
这段代码定义了一个类和一个函数,两者都接受shared_ptr对象作为参数,特别注意的是我们没有使用引用的方式传递参数,而是直接拷贝,就像是在使用一个原始指针,shared_ptr支持这样的用法。
在声明了shared_ptr和两个shared类实例后,指针被它们所共享,因此引用计数为3。print_func()函数内部拷贝了一个shared_ptr对象,因此引用计数再增加1,但当退出函数时拷贝自动析构,引用计数又恢复为3。
程序的运行结果如下:
count:3 v=100 count:3 v=100 count:4 v=20 count:3 v=20
3.4.4 工厂函数
shared_ptr很好地消除了显式的delete调用,如果读者掌握了它的用法,可以肯定delete将会在你的编程字典中彻底消失 。
但这还不够,因为shared_ptr的构造还需要new调用,这导致了代码中的某种不对称性。虽然shared_ptr很好地包装了new表达式,但过多的显式new操作符也是个问题,它应该使用工厂模式来解决。
因此,shared_ptr在头文件<boost/make_shared.hpp> 中提供了一个自由工厂函数(位于boost名字空间)make_shared<T>(),来消除显式的new调用,它的名字模仿了标准库的make_pair(),声明如下:
- template<class T, class... Args>
- shared_ptr<T> make_shared( Args && ... args );
make_shared()函数可以接受最多10个参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr<T>的对象并返回。make_shared()函数要比直接创建shared_ptr对象的方式快且高效,因为它内部仅分配一次内存,消除了shared_ptr构造时的开销。
下面的代码示范了make_shared()函数的用法:
- #include <boost/make_shared.hpp>
- int main()
- {
- shared_ptr<string> sp =
- make_shared<string>("make_shared");//创建string的共享指针
- shared_ptr<vector<int> > spv =
- make_shared<vector<int> >(10, 2); //创建vector的共享指针
- assert(spv->size() == 10);
- }
make_shared()不能接受任意多数量的参数构造对象,一般情况下这不会成为问题。实际上,很少有如此多的参数的函数接口,即使有,那也会是一个设计的不够好的接口,应该被重构。
除了make_shared(),smart_ptr库还提供一个allocate_shared(),它比make_ shared()多接受一个定制的内存分配器类型参数,其他方面都相同。
3.4.5 应用于标准容器
有两种方式可以将shared_ptr应用于标准容器(或者容器适配器等其他容器)。
一种用法是将容器作为shared_ptr管理的对象,如shared_ptr<list<T> >,使容器可以被安全地共享,用法与普通shared_ptr没有区别,我们不再讨论。
另一种用法是将shared_ptr作为容器的元素,如vector<shared_ptr<T> >,因为shared_ptr支持拷贝语义和比较操作,符合标准容器对元素的要求,所以可以实现在容器中安全地容纳元素的指针而不是拷贝。
标准容器不能容纳auto_ptr,这是C++标准特别规定的(读者永远也不要有这种想法)。标准容器也不能容纳scoped_ptr,因为scoped_ptr不能拷贝和赋值。标准容器可以容纳原始指针,但这就丧失了容器的许多好处,因为标准容器无法自动管理类型为指针的元素,必须编写额外的大量代码来保证指针最终被正确删除,这通常很麻烦很难实现。
存储shared_ptr的容器与存储原始指针的容器功能几乎一样,但shared_ptr为程序员做了指针的管理工作,可以任意使用shared_ptr而不用担心资源泄漏。
下面的代码示范了将shared_ptr应用于标准容器的用法:
- #include <boost/make_shared.hpp>
- int main()
- {
- typedef vector<shared_ptr<int> > vs;//一个持有shared_ptr的标准容器类型
- vs v(10); //声明一个拥有10个元素的容器,元素被
- //初始化为空指针
- int i = 0;
- for (vs::iterator pos = v.begin(); pos != v.end(); ++pos)
- {
- (*pos) = make_shared<int>(++i); //使用工厂函数赋值
- cout << *(*pos) << ", "; //输出值
- }
- cout << endl;
- shared_ptr<int> p = v[9];
- *p = 100;
- cout << *v[9] << endl;
- }
这段代码需要注意的是迭代器和operator[]的用法,因为容器内存储的是shared_ptr,我们必须对迭代器pos使用一次解引用操作符*以获得shared_ptr,然后再对shared_ptr使用解引用操作符*才能操作真正的值。*(*pos)也可以直接写成**pos,但前者更清晰,后者很容易让人迷惑。vector的operator[]用法与迭代器类似,也需要使用*获取真正的值。
3.4.6 应用于桥接模式
桥接模式(bridge)是一种结构型设计模式,它把类的具体实现细节对用户隐藏起来,以达到类之间的最小耦合关系。在具体编程实践中桥接模式也被称为pimpl或者handle/body惯用法,它可以将头文件的依赖关系降到最小,减少编译时间,而且可以不使用虚函数实现多态。
scoped_ptr和shared_ptr都可以用来实现桥接模式,但shared_ptr通常更合适,因为它支持拷贝和赋值,这在很多情况下都是有用的,比如可以配合容器工作。
本书不可能完整讲述桥接模式和pimpl惯用法的所有细节,仅通过一个小例子来说明shared_ptr如何用于pimpl。
首先我们声明一个类sample,它仅向外界暴露了最小的细节,真正的实现在内部类impl,sample用一个shared_ptr来保存它的指针:
- class sample
- {
- private:
- class impl; //不完整的内部类声明
- shared_ptr<impl> p; //shared_ptr成员变量
- public:
- sample(); //构造函数
- void print(); //提供给外界的接口
- };
在sample的cpp中完整定义impl类和其他功能:
- class sample::impl //内部类的实现
- {
- public:
- void print()
- { cout << "impl print" << endl;}
- };
- sample::sample():p(new impl){} //构造函数初始化shared_ptr
- void sample::print() //调用pimpl实现print()
- { p->print();}
最后是桥接模式的使用,很简单:
- sample s;
- s.print();
桥接模式非常有用,它可以任意改变具体的实现而外界对此一无所知,也减小了源文件之间的编译依赖,使程序获得了更多的灵活性。而shared_ptr是实现它的最佳工具之一,它解决了指针的共享和引用计数问题。(关于桥接模式更详细的讨论请见推荐书目[1])
9.4.10小节(368页)有另一个真实的pimpl用法例子,但很遗憾它没有用到智能指针。
3.4.7 应用于工厂模式
工厂模式是一种创建型设计模式,这个模式包装了new操作符的使用,使对象的创建工作集中在工厂类或者工厂函数中,从而更容易适应变化,make_shared()就是工厂模式的一个很好的例子。
但由于C++不能高效地返回一个对象,在程序中编写自己的工厂类或者工厂函数时通常需要在堆上使用new动态分配一个对象,然后返回对象的指针。这种做法很不安全,因为用户很容易忘记对指针调用delete,存在资源泄漏的隐患。
使用shared_ptr可以解决这个问题,只需要修改工厂方法的接口,不再返回一个原始指针,而是返回一个被shared_ptr包装的智能指针,这样可以很好地保护系统资源,而且会更好地控制对接口的使用。
接下来我们使用代码来解释shared_ptr应用于工厂模式的用法,首先实现一个纯抽象基类,也就是接口类:
- class abstract //接口类定义
- {
- public:
- virtual void f() = 0;
- virtual void g() = 0;
- protected:
- virtual ~abstract(){} //注意这里
- };
注意abstract的析构函数,被定义为保护的,意味着除了它自己和它的子类,其他任何对象都无权调用delete来删除它。
然后我们再定义abstract的实现子类:
- class impl:public abstract
- {
- public:
- virtual void f()
- { cout << "class impl f" << endl; }
- virtual void g()
- { cout << "class impl g" << endl; }
- };
随后的工厂函数返回基类的shared_ptr:
- shared_ptr<abstract> create()
- { return shared_ptr<abstract>(new impl);}
这样我们就完成了全部工厂模式的实现,现在可以把这些组合起来:
- int main()
- {
- shared_ptr<abstract> p = create(); //工厂函数创建对象
- p->f(); //可以像普通指针一样使用
- p->g(); //不必担心资源泄漏,shared_ptr会自动管理指针
- }
由于基类abstract的析构函数是保护的,所以用户不能做出任何对指针的破坏行为,即使是用get()获得了原始指针:
- abstract *q = p.get(); //正确
- delete q; //错误
这段代码不能通过编译,因为无法访问abstract的保护析构函数。
但这不是绝对的,使用“粗鲁”的方法也可以在shared_ptr外删除对象,因为impl的析构函数是公开的,所以:
- impl *q = (impl*)(p.get());
- delete q; //ok
这样就可以任意操作原本处于shared_ptr控制之下的原始指针了,但最好永远也不要这样做,因为这会使shared_ptr在析构时删除可能已经不存在的指针,引发未定义行为。
3.4.8 定制删除器
在3.4.2小节,我们特意没有讨论shared_ptr一种形式的构造函数shared_ptr(Y * p, D d),它涉及shared_ptr的另一个重要概念:删除器。
shared_ptr(Y * p, D d)的第一个参数是要被管理的指针,它的含义与其他构造函数的参数相同。而第二个删除器参数d则告诉shared_ptr在析构时不是使用delete来操作指针p,而要用d来操作,即把delete p换成d(p)。
在这里删除器d可以是一个函数对象,也可以是一个函数指针,只要它能够像函数那样被调用,使得d(p)成立即可。对删除器的要求是它必须是可拷贝的,行为必须也像delete那样,不能抛出异常。
为了配合删除器的工作,shared_ptr提供一个自由函数get_deleter(shared_ptr<T> const & p),它能够返回删除器的指针。
有了删除器的概念,我们就可以用shared_ptr实现管理任意资源。只要这种资源提供了它自己的释放操作,shared_ptr就能够保证自动释放。
假设我们有一组操作socket的函数,使用一个socket_t类:
- class socket_t {...}; //socket类
- socket_t* open_socket() //打开socket
- {
- cout << "open_socket" << endl;
- return new socket_t;
- }
- void close_socket(socket_t * s) //关闭socket
- {
- cout << "close_socket" << endl;
- //...其他操作,释放资源
- }
那么,socket资源对应的释放操作就是函数close_socket(),它符合shared_ptr对删除器的定义,可以用shared_ptr这样管理socket资源:
- socket_t *s = open_socket();
- shared_ptr<socket_t> p(s, close_socket); //传入删除器
在这里删除器close_socket()是一个自由函数,因此只需要把函数名传递给shared_ptr就可以了。在函数名前也可以加上取地址操作符&,效果是等价的:
- shared_ptr<socket_t> p(s, &close_socket); //传入删除器
这样我们就使用shared_ptr配合定制的删除器管理了socket资源。当离开作用域时,shared_ptr会自动调用close_socket()函数关闭socket,再也不会有资源遗失的担心。
再例如,对于传统的使用struct FILE的C文件操作,也可以使用shared_ptr配合定制删除器自动管理,像这样:
- shared_ptr<FILE> fp(fopen("./1.txt","r"), fclose);
当离开作用域时,shared_ptr会自动调用fclose()函数关闭文件。
shared_ptr的删除器在处理某些特殊资源时非常有用,它使得用户可以定制、扩展shared_ ptr的行为,使shared_ptr不仅仅能够管理内存资源,而是成为一个“万能”的资源管理工具。
3.4.9 高级议题
本小节讨论关于shared_ptr的一些高级议题。
shared_ptr<void>
shared_ptr<void>能够存储void*型的指针,而void*型指针可以指向任意类型,因此shared_ptr<void>就像是一个泛型的指针容器,拥有容纳任意类型的能力。
但将指针存储为void*同时也丧失了原来的类型信息,为了在需要的时候正确使用,可以用static_pointer_cast<T>等转型函数重新转为原来的指针。但这涉及到运行时动态类型转换,它会使代码不够安全,建议最好不要这样使用。
删除器的高级用法
基于shared_ptr<void>和定制删除器,shared_ptr可以有更惊人的用法。由于空指针可以是任何指针类型,因此shared_ptr<void>还可以实现退出作用域时调用任意函数。例如:
- void any_func(void* p) //一个可执行任意功能的函数
- { cout << "some operate" << endl;}
- int main()
- {
- shared_ptr<void> p((void*)0,any_func); //容纳空指针,定制删除器
- } //退出作用域时将执行any_func()
shared_ptr<void>存储了一个空指针,并指定了删除器是操作void*的一个函数,因此当它析构时会自动调用函数any_func(),从而执行任意我们想做的工作。
其他高级用法
shared_ptr的功能已经远远超出了智能指针的范围,除了以上的用法外它还有很多其他用途,如包装成员函数、延时释放等,限于篇幅本书不作详细介绍,读者可参考Boost说明文档。
3.5 shared_array
shared_array类似shared_ptr,它包装了new[]操作符在堆上分配的动态数组,同样使用引用计数机制为动态数组提供了一个代理,可以在程序的生命周期里长期存在,直到没有任何引用后才释放内存。
3.5.1 类摘要
shared_array的类摘要如下:
- template<class T> class shared_array {
- public:
- explicit shared_array(T * p = 0);
- template<class D> shared_array(T * p, D d);
- ~shared_array();
- shared_array(shared_array const & r);
- shared_array & operator=(shared_array const & r);
- void reset(T * p = 0);
- template<class D> void reset(T * p, D d);
- T & operator[](std::ptrdiff_t i) const() const;
- T * get() const;
- bool unique() const;
- long use_count() const;
- void swap(shared_array<T> & b);
- };
shared_array的接口与功能几乎是与shared_ptr是相同的,主要区别如下:
- 构造函数接受的指针p必须是new[]的结果,而不能是new表达式的结果;
- 提供operator[]操作符重载,可以像普通数组一样用下标访问元素;
- 没有*、->操作符重载,因为shared_array持有的不是一个普通指针;
- 析构函数使用delete[]释放资源,而不是delete。
3.5.2 用法
shared_array就像是shared_ptr和scoped_array的结合体——即具有shared_ptr的优点,也具有scoped_array的缺点。有关shared_ptr和scoped_array的讨论大都适合它,因此这里不再详细讲解,仅给出一个小例子说明:
- #include <boost/smart_ptr.hpp>
- using namespace boost;
- int main()
- {
- int *p = new int[100]; //一个动态数组
- shared_array<int> sa(p); //shared_array代理动态数组
- shared_array<int> sa2 = sa; //共享数组,引用计数增加
- sa[0] = 10; //可以使用operator[]访问元素
- assert(sa2[0] == 10);
- } //离开作用域,自动删除动态数组
同样的,在使用shared_array重载的operator[]时要小心,shared_array不提供数组索引的范围检查,如果使用了超过动态数组大小的索引或者是负数索引将引发可怕的未定义行为。
shared_array能力有限,多数情况下它可以用shared_ptr<std::vector>或者std::vector<shared_ptr>来代替,这两个方案具有更好的安全性和更多的灵活性,而所付出的代价几乎可以忽略不计3.6 weak_ptr
weak_ptr是为配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->。它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
3.6.1 类摘要
weak_ptr的类摘要如下:
- template<class T> class weak_ptr
- {
- public:
- weak_ptr();
- template<class Y> weak_ptr(shared_ptr<Y> const & r);
- weak_ptr(weak_ptr const & r);
- ~weak_ptr();
- weak_ptr & operator=(weak_ptr const & r);
- long use_count() const;
- bool expired() const;
- shared_ptr<T> lock() const;
- void reset();
- void swap(weak_ptr<T> & b);
- };
weak_ptr的接口很小,正如它的名字,是一个“弱”指针,但它能够完成一些特殊的工作,足以证明它的存在价值。
3.6.2 用法
weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,在weak_ptr析构时也不会导致引用计数减少,它只是一个静静的观察者。
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr管理的资源)已经不复存在。
weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这正是它“弱”的原因。但它可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
下面的代码示范了weak_ptr的用法:
- shared_ptr<int> sp(new int(10)); //一个shared_ptr
- assert(sp.use_count() == 1);
- weak_ptr<int> wp(sp); //从shared_ptr创建weak_ptr
- assert(wp.use_count() == 1); //weak_ptr不影响引用计数
- if (!wp.expired()) //判断weak_ptr观察的对象是否失效
- {
- shared_ptr<int> sp2 = wp.lock(); //获得一个shared_ptr
- *sp2 = 100;
- assert(wp.use_count() == 2);
- } //退出作用域,sp2自动析构,引用计数减1
- assert(wp.use_count() == 1);
- sp.reset(); //shared_ptr失效
- assert(wp.expired());
- assert(!wp.lock()); //weak_ptr将获得一个空指针
3.6.3 获得this的shared_ptr
weak_ptr的一个重要用途是获得this指针的shared_ptr,使对象自己能够生产shared_ptr管理自己:对象使用weak_ptr观测this指针,这并不影响引用计数,在需要的时候就调用lock()函数,返回一个符合要求的shared_ptr供外界使用。
这个解决方案被实现为一个惯用法,在头文件<boost/enable_shared_from_this. hpp>定义了一个助手类enable_shared_from_this<T>,它的声明摘要如下:
- template<class T>
- class enable_shared_from_this
- {
- public:
- shared_ptr<T> shared_from_this();
- };
使用的时候只需要让想被shared_ptr管理的类从它继承即可,成员函数shared_from_this()会返回this的shared_ptr。例如:
- #include <boost/enable_shared_from_this.hpp>
- #include <boost/make_shared.hpp>
- class self_shared: //一个需要用shared_ptr自我管理的类
- public enable_shared_from_this<self_shared>
- {
- public:
- self_shared(int n):x(n){}
- int x;
- void print()
- { cout << "self_shared:" << x << endl; }
- };
- int main()
- {
- shared_ptr<self_shared> sp = make_shared<self_shared>(314);
- sp->print();
- shared_ptr<self_shared> p = sp->shared_from_this();
- p->x = 1000;
- p->print();
- }
需要注意的是千万不能从一个普通对象(非shared_ptr)使用shared_from_this()获取shared_ptr,例如:
- self_shared ss;
- shared_ptr<self_shared> p = ss.shared_from_this(); //错误!
这样虽然语法上正确,编译也无问题,但在运行时会导致shared_ptr析构时企图删除一个栈上分配的对象,发生未定义行为。