资源管理 Resource Management

所谓资源就是,一旦用了它,将来必须还给系统。C++程序中最常使用的资源就是动态分配内存(如果不归还它,会导致内存泄漏),但内存只是需要管理的众多资源之一。其他常见的资源还包括文件描述符(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接以及网络sockets。不论哪一种资源,重要的是,在不再使用它的时候,必须将它还给系统。

13、以对象管理资源

Use objects to mangage resources.

假设有一个用来模拟投资行为的程序库,其他各式各样的投资类型继承自一个class Investment,然后这个程序库通过一个工厂函数创建特定的Investment对象:

class Investment {   };  //继承体系的基类
Investment* createInvestment(); //返回指针,指向动态分配对象。

对于上述工厂函数的调用者有责任删除它:

void f()
{
Investment* pInv = createInvestment(); //调用factory函数
//一些操作
delete pInv; //释放pInv所指对象
}

上述代码有很多情况无法删除它得自createInvestment的对象:如“…”区域内的一个过早的return语句,或者某个循环里的continue或goto语句的过早退出,“…”区域内语句抛出异常。对于delete被略过,将泄漏对象所保存的任何资源。为解决此问题,可以将资源放进对象内,利用C++的析构函数自动调用机制确保资源被释放。标准库提供的auto_ptr正是针对这种形势而设计的。auto_ptr是个类指针(pointer-like)对象,也就是所谓的智能指针,其析构函数自动对齐所指对象调用delete。使用如下:

void f()
{
std::auto_ptr<Investment> pInv(createInvestment());

}

此实现利用两个关键想法:获得资源后立刻放进管理对象内(“以对象管理资源”又称“资源取得时便是初始化时”(Resource Acquisition Is Initialization:RAII)),管理对象运用析构函数确保资源被释放。

由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意不能让多个auto_ptr同时指向同一个对象。为了预防此问题,auto_ptrs有一个性质,若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权:

std::auto_ptr<Investment> pInv1(createInvestment());  // pInv1指向返回的对象
std::auto_ptr<Investment> pInv2(pInv1); // pInv2指向对象,pInv1被设为null
pInv1 = pInv2; // pInv1指向对象,pInv2被设为null

但是如此受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它,但是比如对于STL容器要求其元素发挥正常的复制行为,此时无法使用auto_ptr。auto_ptr的替代方案是“引用计数型智慧指针”(references-counting smart pointer:RCSP)。所谓的RCSP也是个智能指针,持续追踪共有多少对象指向某个资源,并在无人指向它时自动删除该资源。其无法打破环状引用(cycles of references,例如两个其实已经没被使用的对象彼此互指)。

TR1的tr1::shared_ptr就是个RCSP,所以可以这样实现f:

void f()
{
std::tr1::shared_ptr<Investment> pInv(createInvestment());//调用factory函数

} //经由shared_ptr析构函数自动删除pInv

看起来和auto_ptr差不多,但是shared_ptr复制行为就很正常:

void f()
{
std::tr1::shared_ptr <Investment> pInv1(createInvestment()); // pInv1指向返回的对象
std::tr1::shared_ptr <Investment> pInv2(pInv1); // pInv1和pInv2指向同一个对象,
pInv1 = pInv2; // pInv1和pInv2指向同一个对象。

} // pInv1和pInv2被销毁,它们所指的对象也被自动销毁。
  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获取资源并在析构函数中释放资源。
  • 两个常被使用的RAII
    classes分部是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr复制动作会使被复制物指针指向null。

14、在资源管理类中小心copying行为

Think carefully about copying behavior in resource-mangaging classes.

对于有些不是heap-based资源,像tr1::shared_ptr和auto_ptr这样的智能指针往往不适合作为资源掌管者,有可能需要建立自己的资源管理类。比如使用C API函数处理类型为Nutex的互斥器对象(mutex object),共有lock和unlock两个函数可用:

void lock(Mutex* pm);     //锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥器解除锁定
为确保绝对不会忘记将一个被锁住的Mutex解锁,需要建立一个class用来管理互斥锁,使用RAII守则:
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm)
{ lock(mutexPtr); } //获得资源
~Lock() { unlock(mutexPtr); } //释放资源
private:
Mutex * mutexPtr;
};

客户对Lock的用法符合RAII方式:

Mutex m;    //定义需要的互斥器

{ //建立一个区块用来定义critical section
Lock m1(&m) //锁定互斥器
// 执行critical section内的操作
} // 在区块最末尾自动解除互斥器锁定

但是对于如果复制m,此时可以的处理如下:

禁止复制。很多时候对于允许RAII对象被复制并不合理,比如像Lock这样的class,因为很少能够合理拥有“同步化基础器物”的副本。所以应该将copying操作声明为private。或者如下实现:

class Lock: private Uncopyable{ //禁止复制
public:

};

对底层资源使用“引用计数法”。可以使用tr1::shared_ptr,因为其允许指定所谓的删除器,可以是一个函数或者函数对象,当引用次数为0时便被调用,删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数。如此可以如下实现:

class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm, unlockj)
{ lock(mutexPtr.get());}//以Mutex初始化shared_ptr,并以unlock函数为删除器
private:
tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr替换raw pointer
};

此时不需要声明构造函数,因为不需要,因为在引用次数为0时自动调用tr1::shared_ptr的删除器。

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII class copying行为是:抑制copying、实行引用计数法(references counting)。

15、在资源管理类中提供对原始资源的访问

Provide access to raw resource in resource-managing classes.

资源管理类(resource-managing classes)很强大,可以避免很多资源泄漏,但是许多API需要直接访问原始资源,这样需要绕过资源管理对象直接访问原始资源。例如,使用智能指针保存factory函数如createInvestment的调用结果:

std::tr1::shared_ptr <Investment> pInv(createInvestment());

某个函数处理Investment对象:

int daysHeld(const Investment* pi);

调用它:

int days = daysHeld(pInv);//错误

此时,代码会编译不过,因为daysHeld需要的是Investment*指针,但是传递给的是std::tr1::shared_ptr 的对象。这时候需要一个函数将RAII对象换为其所内含的原始资源。有两个做法:

tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式换,也就是会返回智能指针内部的原始指针:

int days = daysHeld(pInv.get());

和所有的智能指针一样tr1::shared_ptr和auto_pt也重载了指针取值(pointer dereferencing)操作符(operator->和operator*),它允许隐式换至底部原始指针:

class Investment{
public:
bool isTaxFree() const;

};
Investment* createInvestment(); //factory函数
std::tr1::shared_ptr <Investment> pi1(createInvestment());//tr1::shared_ptr管理一笔资源
bool taxable1 = !(pi1->isTaxFree()); //经由operator->访问资源
std::tr1::shared_ptr <Investment> pi2(createInvestment());//tr1::shared_ptr管理一笔资源
bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源

由于有时候的必须取得RAII对象内的原始资源,可以提供一个隐式换函数。例如下面用于字体的RAII class(对C API而言字体是一种原始数据结构):

FontHandle getFont();    //C API
void releaseFont(FontHandle fh); //C API
class Font {
public:
explicit Font(FontHandle fh):f(fh) //获得资源,采用pass-by-value
{ }
~ Font() { releaseFont(f); } //释放资源
private:
FontHandle f; //原始字体资源
};

假设由大量与字体相关的C API,它们处理的是FontHandles,那么将Font对象换为FontHandle会是一个很频繁的需求,可以提供一个显式转换函数,像get一样,但是这样每当使用API时就必须调用get,如此大规模使用,代码可读性变差,而且增加了泄漏字体的可能性,而设计Font class的主要设计目的时为了防止资源(字体)泄漏。所以提供一个隐式转换函数,转型为FontHandle:

class Font {
public:

operator FontHandle() const //隐式转换函数
{ return f; }

};

如下使用:

Font f(getFont());
int newFontSize;

changeFontSize(f, newFontSize); //将Font隐式转换为FontHandle
但是如此做会增加错误发生机会,如在需要Font时意外创建一个FontHandle:
Font f1(getFont());

FontHandle f2 = f1; //原意要拷贝一个Font对象,
//却反而将f1隐式转换为其底部的FontHandle,然后才复制它。

上述程序的FontHandle由Font对象f1管理,但是FontHandle也可通过直接使用f2取得。这样如果当f1被销毁,字体被释放,而f2会成为虚吊的(dangle)。

  • APIs往往要求访问原始资源(raw resource),所以每一个RAII class应该提供一个“取得其所管理的资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

16、成对使用new和delete时要采用相同形式

Use the same form in corresponding uses of new and delete.

例如以下代码:

std:: string* stringArray = new std:: string[100];

delete stringArray;

看起来程序写的很没啥问题,但是stringArray含有100各string对象中的99各不太可能被适当的删除,因为它们的析构函数很可能没被调用。

当使用new(通过new动态生成一个对象),由两种情况,第一,内存被分配处理啊,第二,针对此内存会有一个(或更多)构造函数被调用。当使用delete也有两件事发生,针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放。

当对也给指针使用delete,唯一能够让delete知道内存中是否存在一个“数组大小记录”的办法就是,由调用者告诉它,使用delete时加上中括号,delete便认定指针指向一个数组,否则便认定指针指向单一对象:

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];

delete stringPtr1; //删除一个对象
delete [] stringPtr2; //删除一个由对象组成的数组

对于使用new创建typedef类型的对象时,要特别注意原始类型,防止出现错误。因为C++标准程序库含有string,vector等templates,可将数组的需求降至几乎为零。

  • 如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

17、以独立语句将newed对象置入智能指针

Store newed objecs in smart pointers in standalone statements.

假设有个函数用来指示处理程序的优先权,另一个函数用来在某些动态分配所得的Widget上进行某些带有优先权的处理:

int priority();
void processWidget(std::tr1::shared_ptr<Widget>pw, int priority);

根据以对象管理资源的思路,processWidget决定对其动态分配得来的Widget使用智能指针。如下调用processWidget:

processWidget(new Widget, priority());  //这样参数类型不一致无法编译
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());//可以通过编译了。

但是在此使用对象管理式资源,调用却可能泄漏资源。编译器产出一个processWidget调用码之前,必须首先核算即将被传递的各个实参。上次第一个实参std::tr1::shared_ptr(new Widget)由两部分组成:执行new Widget表达式;调用std::tr1::shared_ptr构造函数。所以在调用processWidget之前,编译器必须创建代码,做以下三件事:调用priority;执行new Widget表达式;调用std::tr1::shared_ptr构造函数。但是编译器执行的顺序不一定时上述理想的方式,如果是执行new Widget表达式;调用priority;调用std::tr1::shared_ptr构造函数,在对priority调用时导致异常,此时new Widge的指针将会遗失,因为还没有被置入std::tr1::shared_ptr内,此时可能引发资源泄漏。为避免此问题,分离语句:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
  • 以独立语句将newed对象存储于(置于)智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

上一篇: ​​C++进阶_Effective_C++第三版(二) 构造/析构/赋值运算 Constructors,Destructors,and Assignment Operators​

下一篇: ​​C++进阶_Effective_C++第三版(四) 设计与声明 Designs and Declarations​