所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。C++程序中最常见使用的资源就是动态分配内存,导致的内存泄漏,但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets.不论哪一种资源,重要的是,当你不再使用它时,必须还给系统。
条款13 以对象管理资源
许多资源被动态分配与heap内而后被用于单一区块或函数内,他们应该在控制流离开那个区块或者函数时被释放。标准库通的auto_ptr正是针对这种形势特制的。auto_ptr是个“类指针对象”,也就是所谓的智能指针,其析构函数自动对其所指对象调用delete。用法如下:
1
2 3 4 5 |
void f()
{ std::auto_ptr<Investment> pInv(createInvestment()); //调用工厂函数,一如既往的使用pInv,经由auto_ptr的析构函数自动删除pInv } |
获得资源后立刻放进管理对象内。(RAII:资源获取实际便是初始化时机)
管理对象运行析构函数确保资源被释放。
上面给出了auto_ptr的智能指针有个问题就是,一定不能让多个auto_ptr同时指向同一个对象,原因在于auto_ptrs有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将获得资源的唯一拥有权,见如下代码:
1
2 3 4 5 6 |
std::auto_ptr<Investment> pInv1(createInvestment());
//pInv1指向createInvestment返回对象 std::auto_ptr<Investment> pInv2(pInv1); //pInv2指向createInvestment返回对象 ,pInv1为NULL pInv1=pInv2; //pInv1指向createInvestment返回对象,pInv2为NULL |
解决方案二:
auto_ptr的替代方案是“引用计数智慧指针(RCSP)”,所谓的RCSPs也是智能指针,持续跟踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收,不同的是RCSPs无法打破环状引用,具体用法如下所示
1
2 3 4 5 6 7 8 9 10 11 12 |
void f()
{ std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment返回对象 std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv2和pInv1同一个对象 pInv1 = pInv2; //同上 ... //pInv2和pInv1b被销毁 //他们所指的对象同样被销毁 } |
不适用情况:
auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作,因此动态分配而得到的array身上适用这两个智能指针是个坏主意。
1
2 |
std::auto_ptr<std::string> aps(
new std::string[
10]);
//坏主意,能编译 std::tr1::shared_ptr< int> spi( new int[ 1024]); //相同问题 |
请记住:
为了防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常用的RAII分别是tr1::shared_ptr和auto_ptr。前者通常是较佳的选择,因为其copy行为比较直观。若选择auto_ptr,复制它动作是它指向null。
条款14 在资源管理类中小心copying行为
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Lock
{ public: explicit Lock(Mutex *pm):mutexPtr(pm) { lock(mutexPtr); //获得资源 } ~Lock() { unlock(mutexPtr); //释放资源 } private: Mutex *mutexPtr; } //定义互斥器 Mutex m; ... //建立一个区块用来定义critical section Lock m1(&m); //锁定互斥器 ... //执行critical section 内的操作、在区块最末尾,自动解除互斥器锁定 ///如果Lock对象被复制,会发生什么呢? Lock m11(&m); //锁定m Lock m12(m11); //复制 |
1
2 3 4 5 |
class Lock :
private Uncopyable
//禁止复制 { public: ... }; |
1
2 3 4 5 6 7 8 9 10 11 |
class Lock
{ public: explicit Lock(Mutex *pm) //以某个Mutex初始化shard_ptr并以unlock函数 : mutexPtr(pm, unlock) //为删除器 { lock(mutexPtr.get()); //条款15谈到 get } private: std::tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr }; |
条款15 在资源管理类中提供对原始资源的访问
1.如何访问原始资源
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// pro_acce.cpp : 定义控制台应用程序的入口点。 //2011/9/21 by wallwind on sunrise; #include "stdafx.h" #include <iostream> #include <memory> using namespace std; class myClass { public : myClass() { cout<< "myClass()"<<endl; } ~myClass() { cout<< "~myClass()"<<endl; } void printFunc() { cout<< "printFunc()"<<endl; } int getType() const { return type; } private: int type; }; myClass* creatMyClass() { myClass *my= new myClass(); return my; } void issue(myClass *my) { delete my; } int _tmain( int argc, _TCHAR* argv[]) { auto_ptr<myClass> apMy(creatMyClass()); myClass* myC=apMy.get(); //auto_ptr 给我们提供的函数,用来访问原始资源 myC->printFunc(); //调用了myClass的方法 return 0; } |
tr1::shared_ptr 和 auto_ptr 都提供了一个 get 成员函数来进行显式转换,也就是说,返回一个智能指针对象中的裸指针(的副本):
myClass* myC=apMy.get();//
似乎所有的智能指针类,包括 tr1::shared_ptr 和 auto_ptr 等等,都会重载指针解析运算符( operator-> 和 operator* ),这便使得对原始裸指针进行隐式转换成为现实,在这里我就不实际举例子了。下面使用书中的片段代码来说明一下问题吧:
1
2 3 4 5 6 7 8 9 10 11 |
std::tr1::shared_ptr<Investment> pi1(createInvestment());
// 使用 tr1::shared_ptr // 管理资源 bool taxable1 = !(pi1->isTaxFree()); // 通过 operator-> 访问资源 ... std::auto_ptr<Investment> pi2(createInvestment()); // 使用 auto_ptr 管理资源 bool taxable2 = !((*pi2).isTaxFree()); // 通过 operator* 访问资源 |
1
2 3 4 5 6 7 8 9 10 11 12 |
void issue(myClass *my)
{ delete my; } //这个函数没有使用到,那么当你使用这个时候 int _tmain( int argc, _TCHAR* argv[]) { auto_ptr<myClass> apMy(creatMyClass()); issue(apMy.get()); return 0; } |
如果让资源管理类型提供隐式转换函数,可以让行为变的更自然,但这样的作法没有好下场,只会增加客户端发生错误的机率。比如下面的代码(简单的编写了一个自定义的AutoPtr,它重载了隐式转换operator):
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#include
"stdafx.h"
#include <stdlib.h> #include <memory> #include <string> using namespace std; template< typename T> class AutoPtr { public: AutoPtr(T *tP) : _tP(tP), _released( false) { } ~AutoPtr() { Release(); } T * operator->() { return _tP; } operator T *() const { return _tP; } void Release( void) { if (!_released) { delete _tP; _released = true; } } private: T *_tP; bool _released; }; typedef struct Point { double X; double Y; }; void PrintPoint(Point *pTP) { } int _tmain( int argc, _TCHAR *argv[]) { AutoPtr<Point> apI( new Point()); PrintPoint(apI); Point *pTP = apI; apI.Release(); system( "pause"); return 0; } |
也许在诸如auto_ptr、shared_ptr只提供原始指针的访问违背了面向对象的封装性,可能是令人产生误解和迷惑的地方。因为RAII classes并不是为了封装某物而存在的,更多的它只是一种模版编程的概念。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
#include
"stdafx.h"
#include <stdlib.h> #include <memory> #include <string> using namespace std; template< typename T> class AutoPtr { public: AutoPtr(T *tP) : _tP(tP), _released( false) { } ~AutoPtr() { Release(); } T * operator->() { return _tP; } operator T *() const { return _tP; } void Release( void) { if (!_released) { delete _tP; _released = true; } } private: T *_tP; bool _released; }; typedef struct Point { double X; double Y; }; void PrintPoint(Point *pTP) { } int _tmain( int argc, _TCHAR *argv[]) { AutoPtr<Point> apI( new Point()); PrintPoint(apI); Point *pTP = apI; apI.Release(); system( "pause"); return 0; } |
也许在诸如auto_ptr、shared_ptr只提供原始指针的访问违背了面向对象的封装性,可能是令人产生误解和迷惑的地方。因为RAII classes并不是为了封装某物而存在的,更多的它只是一种模版编程的概念。
牢记在心
l API 通常需要访问原始资源,因此每个 RAII 类都应该提供一个途径来获取它所管理的资源。
l 访问可以通过显式转换或隐式转换来实现。一般情况下,显式转换更安全,但是隐式转换对于客户端程序员来说使用更方便。
条款16 成对使用new和delete时要采用相同形式
条款17 以独立语句将newed对象置入智能指针
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// store_new.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <memory> using namespace std; class Widget { public: Widget() { cout << "Widget()" << endl; } ~Widget() { cout << "Widget()" << endl; } }; int priority() { throw new runtime_error( "Exception"); //return 0; } void processWidger(auto_ptr<Widget> pw, int priority) {} int _tmain( int argc, _TCHAR *argv[]) { processWidger(auto_ptr<Widget>( new Widget()), priority()); system( "pause"); return 0; } |
1、调用X的构造函数。
2、调用auto_ptr< Widget >的构造函数。
3、调用Priority函数。
看上去井然有序的条件,C++编译器未必会选择这么做。也许编译器选择将调用Priority函数放在第二的位置会生成更高效的代码也说不定。那么顺序就会改为:
1、调用X的构造函数。
2、调用Priority函数。
3、调用auto_ptr< Widget >的构造函数。
那么如果调用Priority函数产生异常怎么办?auto_ptr并没有获得它需要保管的资源,而那段资源也不会遭到释放,有一种资源泄漏的方式。
解决方式就是在外面完成智能指针的存储,编译器对于跨越语句的各项操作不会选择重新排列。这样智能指针依然获得了对所指向资源的保护。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// store_new.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <memory> using namespace std; class Widget { public: Widget() { cout << "Widget()" << endl; } ~Widget() { cout << "Widget()" << endl; } }; int priority() { throw new runtime_error( "Exception"); //return 0; } void processWidger(auto_ptr<Widget> pw, int priority) {} int _tmain( int argc, _TCHAR *argv[]) { auto_ptr<Widget> pw( new Widget()); processWidger(pw, priority()); system( "pause"); return 0; } |
请记住:
■ 以独立语句将newed对象存储于(置入)智能指针内.如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源
泄露.