条款13:以对象管理资源
假设一个类层次,基类是Investment,然后通过一个工厂函数产生某个特定的invertment对象(可能是某个继承类对象),如下:
//基类 class Investment {...} //程序库通过一个工厂函数产生某个特定的inverstment对象,返回指针 Investment* createInvestment(); //这导致调用者需要对这个返回的指针进行删除 void f() { Investment* pInv = createInvestment(); //这里中间可能过早返回,或抛出异常,则都不会执行delete,也就会发生资源泄漏 ... delete pInv; }
C++有两个类可以提供:str::auto_ptr, TR1的tr1::shared_ptr。这两个都可以当ptr被销毁时,对象被销毁。为了防止资源泄漏,应使用RAII对象,它们在构造函数汇总获得资源并在析够函数中释放资源。
注意不能让多个auto_ptr指向同一对象,否则一个对象会被删除多次,严重错误。所以auto为了防止这个问题,有以下不同寻常的性质:若通过copy构造函数或拷贝赋值操作符复制它们,它们会变成null, 而复制所得的指针将取得资源的唯一所有权。由于它的异常拷贝行为,所以它并不是管理动态分配资源的利器。而且不能在STL容器中使用auto_ptr,因为STL容器要求其元素发挥“正常的复制行为”。auto_ptr的行为:
//解决方法1:使用auto_ptr管理对象资源 void f() { //这里是RAII,获得资源后立刻放进管理对象 std::auto_ptr<Investment> pInv(createInvestment); //auto_ptr的复制行为 std::auto_ptr<Investment> pInv2(pInv); //pInv2指向对象,pInv=null pInv = pInv2; //pInv指向对象,pInv2=null }//离开函数的时候,调用auto_ptr的析构函数确保资源被释放auto_ptr的替代方案是“引用计数型智能指针”,它维护一个变量表示共有多少对象指向某笔资源,并在变量变为0时删除对象。类似垃圾回首,但无法打破环状引用。它的拷贝行为正常:
//解决方法2:使用tr1::shared_ptr管理对象资源 void f() { //这里是RAII,获得资源后立刻放进管理对象 std::tr1::shared_ptr<Investment> pInv(createInvestment); //shared_ptr的复制行为 std::tr1::shared_ptr<Investment> pInv2(pInv); //pInv2, pInv指向同一对象 pInv = pInv2; //pInv,pInv2指向同一对象 }//离开函数的时候,调用shared_ptr的析构函数确保pInv,pInv2被释放令auto_ptr与shared_ptr都在其析构函数内做delete而不是delete[],这意味着在动态分配而得的array上使用它们是不应该的,如:
//以下行为虽然可以通过编译器,但是会有资源泄漏,不应该使用 std::auto_ptr<std::string> aps(new std::string[10]); std::tr1::shared_ptr<std::string> api(new int[1024]);
条款14:在资源管理类中小心copying行为
对于某些资源,并不适合使用auto_ptr和shared_ptr管理资源,有可能需要自己写管理资源的类,并遵守RAII,例如以下,Mutex表示互斥对象,lock, unlock对资源管理,可以建一个类来管理这个资源,并且对于这个类的拷贝行为,可以有以下四种行为,如下:
//资源管理类的copying行为 class Lock { public: explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); //构造函数锁住资源 } ~Lock() { unlock(mutexPtr); //析构函数释放资源 } }; //客户端的用法 Mutex m; Lock ml(&m); //锁定,执行关键区域内的操作 ... //如果进行拷贝,可以发生以下情况: Lock ml1(&m); Lock ml2(ml1); //情况1:禁止复制,参考条款6的做法, //1声明为私有,不定义,如果复制发生链接错误 //2建一个不能拷贝的基类,并私有继承于它,这将错误移到编译期 class Lock : private Uncopyable { }; //情况2:引用计数法,例如shared_ptr的行为,但当资源变为0时,它的默认行为是 //删除所指物,而如果我们只是释放锁定,这种情况下可以利用shared_ptr中可以指定“删除器” //的一个函数或函数对象,当引用计数为0时,调用此函数,这个参数对它的构造函数是可有可无的第二个参数 class Lock { public: explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) //unlock即为指定的删除器 { lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; //用shared_ptr管理这个对象,并且指定删除器,自定义行为 }; //而且以上不再需要析够函数,因为析构函数会在引用计数为0时自动调用shared_ptr的删除器 //情况3:复制底部资源,进行深度拷贝,例如string类, //情况4:转移底部所有权,如auto_ptr的行为,资源所有权从被复制物转移到目标物总结:1 赋值RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的拷贝行为 2 普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用 计数法。不过其他行为也都可能实现。
条款15:在资源管理类中提供对原始资源的访问
APIs往往要求访问原始资源,所以每个RAII类应该提供一个“取得其所管理之资源”的方法。
对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。例如shared_ptr与auto_ptr都提供了一个get函数,来返回指向的内部资源。再如下面的例子:
//资源管理类中提供对原始资源的访问 class Font { public: explicit Font(FontHandle fh) : f(fh) {} //可以提供两种访问原始资源的方式: //1 显示转换函数,优点是安全,缺点是每次调用都需要访问该函数 FontHandle get() const { return f; } //2 隐式转换,重载转换操作符,优点不需显示调用,会隐式执行,缺点:容易出错 operator FontHandle() const { return f; } ~Font() { releaseFont(f); } private: FontHandle f; //管理的原始字体资源 };对于隐式的转换会增加错误发生的机会,如下:
Font f1(getFont());
FontHandle f2 = f1; //本来是想拷贝Font对象,却隐式转换f1为FontHandle,然后进行了复制
以上一个潜在的问题是,f1拥有的资源被f2取得,这不会有好下场,例如f1被销毁,字体释放,f2成为虚吊的。
所以获得RAII底部资源,是使用显示调用还是隐式转换,取决于RAII类被设计的特定工作,以及它被使用的情况。条款18有,让接口容易被正确使用,不易被勿用,所以显示的get通常比较受欢迎。
条款16:成对的使用new和delete时采取相同的形式
如果在new表达式中使用[ ], 必须在相应的delete表示式中也使用 [ ], 如果new不使用,delete也不要使用。
这其实就是数组与单个对象的形式。因为单一对象的内存布局一般而言不同于数组内存布局。数组内存通常还包括“数组大小”,以便知道delete需要调用多少次析构函数。单一对象没有这笔记录。
当使用new时,有两件事发生:1 内存会被分配出来(通过operator new) 2针对此内存调用一个(或多个)构造函数;
当使用delete时,有两件事发生:1 析构函数调用 2 内存被释放(通过operator delete)
当你写的一个类中含有一个指针指向动态分配的内存,并提供多个构造函数时,这时要使所有构造函数中使用相同的形式的new将指针成员初始化,因为析构函数中只能有一种delete形式。
最好不要对数组形式做typedefs动作,不然容易发生错误:
typedef std::string AddressLines[4]; //定义了一个AddressLines类型,执行string [4] std::string* pal = new AddressLines; //这里分配的是数组,相当于new string[4] delete pal; //错误,但是可能会发生 delete [] pal; //正确的形式,但与以上new不对称
如以下例子会有隐式的资源泄漏:
//以独立语句将newed对象置入智能指针 //有以下函数 int priority(); void processWidget(std::tr1::shared_ptr<Widget> pw, int priority); //以下调用形式,不会通过编译,因为tr1::shared_ptr的构造函数是explicit函数 //虽然它接受一个widget的指针,但是不能进行隐式转换, processWidget(new Widget, priority()); //所以如果使用以下强制转化,可以通过,但这可能有隐式的资源泄漏 processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
1 执行“new Widget”表达式
2 调用tr1::shared_ptr构造函数
于是在调用processWidget之前,必须做以下三件事:
* 调用priority
* 执行“new Widget”表达式
* 调用tr1::shared_ptr构造函数
但编译器完成以上事情的次序并不一定,但是new Widget一定在shared_ptr构造函数之前,它有可能是以下序列
1 执行“new Widget”表达式
2调用priority
3调用tr1::shared_ptr构造函数
这样可能会有资源泄漏,即如果priority调用过程中发生异常,则new出的指针会丢失!!太隐蔽了。。。为了避免以上事情的发生:使用分离语句 1 创建Widge,构造一个智能指针,2 传给processWidget
//解决方案: std::tr1::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());