您的位置:首页 > 编程语言 > C语言/C++

《Effective C++》读书笔记(四) 资源管理

2013-09-28 18:50 225 查看
资源管理
Resource Management

C++最常使用的资源就是动态分配内存,还有其他常见的资源,比如文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当不再使用它时,必须将它还给系统。否则,资源泄露等糟糕的事情就会接踵而至。并且,当发生异常,或者函数内多重回传路径,也可能维护人员没充分理解就改动软件等情况发生时,资源泄露的可能性就会增加很多。因此,资源管理的特殊手段还需要充分应用。

条款13:以对象管理资源

Use objects to manage resources.
有个例子,使用一个塑模投资行为的程序库,其中各种各样的投资类型继承自一个root class Investment: :
class Investment {...}; //“投资类型”继承中的root class
通过一个函数,可以创建一个特定的Investment对象,并且在调用结束后删除它:
void f()
{
Investment* pInv=createInvestment(); //用一个指针指向调用体系内动态分配对象
... //中途的各种调用
delete pInv; //用完后果断删除
}
但是,当...区域内,随便抛出了一个异常,或者一个过早的return语句,就没办法delete了,最后泄露的,不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。

“以对象管理资源”有两个关键想法:
1.获得资源后立刻放进管理对象(managing object)内。
2.管理对象(managing object)运用析构函数确保资源被释放。
“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)
可以对上个例子进行一些修改,最终达到我们预期的目的。可以利用标准程序库中定义的auto_ptr指针,auto_ptr模板定义了类似指针的对象,当auto_ptr对象过期时,其析构函数会自动对其所指对象使用delete来释放内存。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment( ));
...
//delete pInv    //这里没必要再画蛇添足,auto_ptr指针已经帮忙释放了资源。
//如果去掉了上面的注释,反而会引起pInv未定义而带来的问题
}
看起来不错。但仍需注意,由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr指向同一对象。如果不幸发生了,对象会被发生一次以上,进而引发未定义行为。不过auto_ptr有一个不寻常的性质:若通过copy构造函数或copy
assignment操作符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一所有权。但也正因如此,STL中的容器不适合使用auto_ptr。

一个更好的方法是使用“引用计数型智能指针”(reference-counting smart pointer;RCSP)。它能持续追踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。比如TR1(Technical Report 1)的tr1::shared_ptr就是一个RCSP。
void f()
{
...
std::tr1::shared_ptr<Investment> pInv(createInvestment( ));
...
}


auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[],所以这两者不能用于数组来管理资源;不过可以用boost::scoped_array和boost::shared_array的类对象,它们针对数组而设计,并且管理资源效果较好。

☆为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源

☆两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使被复制物指向null。

条款14:在资源管理类中小心copying行为
Think carefully about copying behavior in resource-managing classes.
条款13表示过:“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。但是,并非所有的资源都是基于堆(heap-based)而实现的,也就说明,并非所有资源都能用auto_ptr或tr1::shared_ptr来管理。有必要的话,还是需要建立自己的资源管理类。
在建立自己的资源管理类时,难免会遇到这样的问题:“当一个RAII对象被复制时,会发生什么事?”
比较常见的方法是禁止复制,对底层资源祭出“引用计数法”(reference-count)。
禁止复制,说的是将copying操作声明为private。
class Lock:private Uncopyable
{
public:
...         //锁定的各种操作
};

对底层资源祭出“引用计数法”(reference-count),适用于当我们希望保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该递增资源的“被引用次数”。tr1::shared_ptr就能做到。

还有两种关于操作RAII class copying的方法:复制底部资源和转移底部资源的拥有权
复制底部资源是针对一份资源拥有其任意数量的副本,当不再需要某个副本时,就应该释放。也就是说复制资源管理对象时,进行的是深度复制。
转移底部资源的拥有权,是指某些罕见场合下希望确保永远只有一个RAII对象指向一个未加工资源(raw resource),即使RAII对象被复制也如此。资源的拥有权会从被复制物转移到目标物。使用auto_ptr在这很合适。

☆复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
☆普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。

条款15:在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes.
资源管理类(resource-managing classes)很棒,它们是对抗资源泄露的堡垒。排除此等泄露是良好设计系统的根本性质。但是,许多APIs直接指涉资源,不使用已经开发好的APIs,效率又太低。就需要一个RAII对象来转换为原始资源类型,再访问原始资源。
一个例子,使用智能指针来保存一个刚创建的特定Investment对象。这个Investment对象就是原始资源。
std::tr1::shared_ptr<Investment> pInv(createInvestment());


假设需要某个函数,来处理Investment对象:

int daysHeld(const Investment* pi);        //返回投资天数


这时候就需要一个函数将RAII对象(auto_ptr或tr1::shared_ptr)转换为原始资源Investment*。auto_ptr和tr1::shared_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件):
int days=daysHeld(pInv.get());

使用显式调用比较安全,但如果每次都要调用.get()会相当麻烦。也有比较方便的方法,就是使用隐式转换。auto_ptr和tr1::shared_ptr重载了operator->和operator*,就是用来隐式转换至底部原始指针的:

class Investment
{
public:
bool isTaxFree()const;
...
};
Investment* createInvestment();
std::tr1::shared_ptr<Investment> pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree());   //operator->
...
std::auto_ptr<Investment> pi2(createInvestment());
bool taxable2 = !((*pi2).isTaxFree());  //operator*

但隐式转换是有风险的。比如下面一个例子,用于字体的RAII class:

FontHandle getFont();       //一个 C API,已简化暂时省略参数
void releaseFont(FontHandle fh);     //同一组C API
class Font
{
public:
explicit Font(FontHandle fh): f(fh)  {}  //C API 要pass by value
operator FontHandle() const {return f; }   //隐式转换函数
~Font() {releaseFont(f); }     //释放资源
...
private:
FontHandle f;                //原始字体资源
};


但如果想要Font对象时,意外地创建一个FontHandle:

Font f1(getFont());
...
FontHandle f2=f1;


原来的意思是要复制一个Font对象,却反而将f1先隐式转换为FontHandle对象,再由f2复制。并且operator=默认先处理右值再处理左值。当FontHandle由使用f2取得时,最后就很容易造成f2成为“虚吊”(dangle)(一个悬而未定的)值。

虽然隐式转换方便,但为了设计更为安全,更容易被正确使用的接口,还是用显式转换好些。

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

条款16:成对使用new和delete时要采用相同形式

Use the same form in corresponding uses of new and delete.
当使用new动态生成一个对象时,有两件事发生。第一,利用operator new,内存被分配出来。第二,针对此内存会有一个(或更多)构造函数被调用。delete的最大问题在于:即将被删除的内存之内究竟存有多少个对象?这个问题的答案就决定了有多少个析构函数要被调用。
由于单一对象的内存布局与数组的内存布局不同,所以对于operator delete和operator delete[]的设计也会应构造函数被创建出来的方式(new 或 new[])来一一对应。
规则很简单:如果你调用new时使用[],那么就必须调用delete时也用[];如果调用new时没有[],那么也不该在对应调用delete时使用[]。尤其是对于经常使用typedef类型对象的时候,更应留意;以免用new时调用delete[],以及new[]时调用delete等造成不良后果

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

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

Store newed objects in smart pointers in standalone statements.

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

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


由于tr1::shared_ptr的构造函数是个explicit,不支持隐式转换,所以需要将new对象的原始指针转换为相对应的智能指针,才能通过processWidget接口的编译。但更需要注意的是,如果将执行语句合并在一起,就像这样:
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority() );
这样并未确定编译顺序。在调用processWidget之前,编译器必须做三件事:
调用priority、执行"new Widget"、调用tr1::shared_ptr构造函数。

当触发了某一种不凑巧的顺序:1.执行"new Widget",2.调用priority,3.调用tr1::shared_ptr构造函数
万一调用priority异常,那么执行过的new Widget就没办法delete,最后就可能引发资源泄露。

要避免这类问题,就使用分离语句。分别写出1.创建Widget 2.将它置入一个智能指针内,再把那个智能指针传给processWidget:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());

这样就避免了上述潜在的问题。

☆以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

参考文献:

《Effective C++》3rd Scott Meyers著,侯捷译
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: