资源管理

来源:互联网 发布:五线谱转简谱软件 编辑:程序博客网 时间:2024/05/09 17:56

资源管理

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

Rule 13 以对象管理资源

假设我们使用一个用来描述投资行为各式各样的投资类型继承自一个root class(例如股票、债券等等)的程序库,其中root class Investment:

class Investment{...};//“投资类型”继承体系中的root class

进一步假设,这个程序库系通过一个工厂函数(factory function,见条款7)供应我们某特定的Investment对象:

Investment* createInvestment();//返回指针,指向Investment继承体系内                                //的动态分配对象。调用者有责任删除它。                                //这里为了简化,刻意不写参数。void f(){Investment* pInv=createInvestment();//调用factory函数  //...delete pInv;//释放pInv所指对象}

这看起来妥当,但若干情况下f可能无法删除它得自createInvestment的投资对象—或许因为”…”区域内的一个过早的return语句。如果这样一个return被执行起来,控制流就绝不会触及delete语句。类似情况发生在对createInvestment的使用及delete动作位于某循环内,而该循环由于某个continue或goto语句过早退出。最后一种可能是”…”区域内的语句抛出异常,果真如此控制流将再次不会幸临delete。无论delete如何被略过去,我们泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。

为确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。实际上这正是隐身于本条款背后的半边想法;把资源放进对象内,我们便可倚赖C++的“析构函数自动调用机制”确保资源被释放。(稍后讨论另半边想法。)

void f(){    std::auto_ptr<Investment> pInv(createInvestment());//调用factory函数                                                      //一如以往地使用pInv                                                      //经由auto_ptr的析构函数自动删除pInv}
  • 获得资源后立刻放进管理对象(managing object )内。以上代码中createInvestment返回的资源被当做其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(ResourceAcquisition Is Initialization; RAII ).

  • 管理对象(managing object)运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。如果资源释放动作可能导致抛出异常,事情变得有点棘手,但条款8己经能够解决这个问题,所以这里我们也就不多操心了。

这里必须注意auto_ptr指针不能同时指向一个资源。某特定时刻只能有一个auto_ptr指向一个实例。

std::auto_ptr<Investment>      pInvl(createInvestment());//pInv1指向createInvestment返回物.      std::auto_ptr<Investment> pInv2(pInvl);//现在PInv2指向对象,pInv1被设为null.pInvl=pInv2;//现在pInvl指向对象,pInv2被设为null.

auto ptr的替代方案是‘引用计数型智慧指针"( reference-counting smart pointer;RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收(garbage collection ),不同的是RCSPs无法打破环状引用(cycles of references,例如两个其牢已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。

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

  • 两个常被使用的RAII classes分别是trl::shared_ptr和auto- ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto- ptr,复制动作会使它(被复制物)指向null.

  • 这种方式适合资源为head-based。

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

条款13导入这样的观念:“资源取得时机便是初始化时机,"< Resource AcquisitionIs Initialization; RAII ),并以此作为“资源管理类”的脊柱,也描述了auto_ptr和trl::shared_ptr如何将这个观念表现在heap-based资源上。然而并非所有资源都是heap-based,对那种资源而言,像auto_ptr和trl::shared_tr这样的智能指针往往不适合作为资源掌管者(resource handlers )。既然如此,有可能偶而你会发现,你需要建立自己的资源管理类。

例如,假设我们使用C API函数处理类型为Mutex的互斥器对象(mutex objects ),共有lock和unlock两函数可用:

void lock(Mutex* pm);//锁定pm所指的互斥器.void unlock(Mutex* pm);//将互斥器解除锁定.

为确保绝不会忘记将一个被锁住的Mutex解锁,你可能会希望建立一个class用来管理机锁。这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”:

class Lock {public:    explicit Lock(Mutex* pm)      :mutexPtr(pm)    {lock(mutexPtr);}//获得资源~Lock(){unlock();}private:unlock(mutexPtr);//释放资源Mutex *mutexPtr;};

问题是当RAII对象被复制时将会发生什么:

Mutex m;//定义你需要的互斥器{//建立一个区块用来定义critical section.Lock ml(&m);//锁定互斥器. ...   //执行critical section内的操作.}//在区块最末尾,自动解除互斥器锁定.这很好,但如果Lock对象被复制,会发生什么事?Lock mll(&m);//锁定mLock ml2(mll);//将mll复制到m12身上。这会发生什么事?
  • 禁止复制。许多时候允许RAII对象被复制并不合理。对一个像Lock这样的class这是有可能的,因为很少能够合理拥有“同步化基础器物”(synchronization primitives)的复件(副本)。如果复制动作对)tAII class并不合理,你便应该禁止之。条款6告诉你怎么做:将copying操作声明为private。对Lock而言看起来是这样:

    class Lock: private Uncopyable{//禁止复制。见条款6  public:  ...};
  • 对底层资源祭出“引用计数法”< reference-count >。有时候我们希望保有资源,直到它的最后一个使用者(某对象被销毁。

    class Lock{public:    explicit Lock(Mutex* pm)//以某个Mutex初始化shared_ptr      :mutexPtr(pm, unlock)//并以unlock函数为删除器.        {          lock(mutexPtr.get());//条款15谈到”get"        }private:    std::trl::share屯ptr<Mutex> mutexPtr;//使用shared ptr};//替换raw pointer

    请注意,本例的Lock class不再声明析构函数。因为没有必要。条款5说过,class析构函数(无论是编译器生成的,或用户自定的)会自动调用其non-static成员变量(本例为mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥器的引用次数为0时自动调用trl::shared_ptr的删除器(本例为unlock)。(当你阅读这个class的原始码,或许会感谢其中有一条注释指出:你并没有忘记析构,你只是倚赖了编译器生成的缺省行为。)

  • 复制底部资源。有时候,只要你喜欢,可以针对一份资源拥有其任意数量的复件(副本)。而你需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包覆的资源。也就是说,复制资源管理对象时,进行的是“深度拷贝”。

  • 转移底部资源的拥有权。如auto_ptr.

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

  • 普遍而常见的RAII class copying行为是:抑制copying施行引用计数法(reference counting )。不过其他行为也都可能被实现。

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

许多APIs直接指涉资源,所以除非你发誓(这其实是一种少有实际价值的举动)永不录用这样的APIs,否则只得绕过资源管理对象(resource-managing objects)直接访问原始资源(raw resources )。

举个例子,条款13导入一个观念:使用智能指针如auto_ptr或trl::shared_ptr保存factory函数如createInvestment的调用结果:

  std::tr1::shared_ptr<Investment> pInv(createInvestment());//见条款13

假设你希望以某个函数处理Investment对象,像这样:

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

你想要这么调用它:

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

却通不过编译,因为daysHeld需要的是Investment*指针,你传给它的却是个类型trl::share_ptr<Investment>的对象。

这时候你需要一个函数可将RAII class对象(本例为trl::shared_ptr)转换为其所内含之原始资源(本例为底部之Investment*)。有两个做法可以达成目标:显式转换和隐式转换。

  • 显示转换

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

    int days=daysHeld(pInv.get());//很好,将pInv内的原始指针 传给daysHeld
  • 隐式转换函数。

    假设有大量与字体相关的C API,它们处理的是FontHandles,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。Font class可为此提供一个显式转换函数,像get那样:

    class Font {public://...FontHandle get()const{return f;}//显式转换函数//...};class Font{public:operator FontHandle()const//隐式转换函数{return f;}};

    隐式转换会导致不可预测错误。

  • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供 一个“取得其所管理之资源”的办法。

  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

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

这个规则对于喜欢使用typedef的人也很重要,因为它意味typedef的作者必须说清楚,当程序员以new创建该种typedef类型对象时,该以哪一种delete形式删除之。考虑下面这个typedef:

typedef std::string AddressLines[4];//每个人的地址有4行,每行是一个string

由于AddressLines是个数组,如果这样使用new:

std::string* pal=new AddressLines;//注意"new AddressLines',返回                                   //一个string*,就像"new string[4]”一样。

那就必须匹配“数组形式”的delete:

delete pal;delete[]pal;//行为未有定义!                          

为避免诸如此类的错误,最好尽量不要对数组形式做typedefs动作。这很容易达成,因为C++标准程序库(条款54)含有string, vector等templates,可将数组的需求降至几乎为零。例如你可以将本例的AddressLines定义为“由strings组成的一个vector”,也就是其类型为vector<string>.

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

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

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

int priority();void processWidget((std::trl::shared ptr<Widget> pw, int priority);

现在考虑调用processWidget:

processWidget(new Widget, priority());

等等,不要考虑这个调用形式。它不能通过编译。trl::shared ptr构造函数需要一个原始指针(raw pointer),但该构造函数是个explicit构造函数,无法进行隐式转换,将得自,"newWidget"的原始指针转换为processWidget所要求的trl:.shared_ptr。如果写成这样就可以通过编译:

processWidget(std::trl::shared_ ptr<Widget>(new Widget),priority());

C++编译器以什么样的次序完成这些事情呢?弹性很大。这和其他语言如Java和C#不同,那两种语言总是以特定次序完成函数参数的核算。可以确定的是’'new Widget”一定执行于trl::shared ptr构造函数被调用之前,因为这个表达式的结果还要被传递作为trl::shared-ptr构造函数的一个实参,但对priority的调用则可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行它(说不定可因此生成更高效的代码,谁知道!),最终获得这样的操作序列:

避免这类问题的办法很简单:使用分离语句,分别写出(1创建Widget(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget:

std::trl::shared ptr<Widget> pw(new Widget);//在单独语句内以智能指针存储newed所得对象  processWidget(pw, priority());//这个调用动作绝不至于造成泄漏。                                            
  • 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

  • C++编译器优化可能会导致顺序错乱,尽量一句一句书写代码。

原创粉丝点击