条款13:以对象管理资源
假设我们使用一个用来塑模投资行为的程序库,其中各式各样的投资类型继承自一个root class Investment:
class Investment{……};
进一步假设,这个程序库通过一个工厂函数供应我们某特定Investment对象:
Investment* createInvestment();
createInvestment的调用端使用了函数返回的对象后,有责任删除之。现在考虑有个f函数履行这个职责:
void f() { Investment* pInv=createInvestment(); ... delete pInv; }
上述f中在若干情况下可能无法删除它得自createInvestment的投资对象,因为“...”区块可能导致过早退出,并没有执行delete,比如return、continue、goto或者异常等情况,因此会产生潜在的内存泄漏风险。
为确保createInvestment返回的资源总是被释放,我们需要将资源对象放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。例如以下,使用标准库auto_ptr只能指针,其析构函数自动对其所指对象调用delete,从而避免内存泄漏:
void f() { std::auto_ptr<Investment> pInv(createInvestment()); }
这个例子示范“以对象管理资源”的两个关键想法:
获得资源后立即放进管理对象内;
管理对象运用析构函数确保资源被释放;
由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。所以auto_ptr若通过拷贝构造函数或拷贝赋值操作符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权。
auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;RCSP),它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。
auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr会使资源得不到释放。
请记住:
为防止资源泄漏,请使用RAII(Resource Acquisitoin Is Initialization,资源取得实际便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的RAII类分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。
条款14:在资源管理类中小心copying行为
当一个RAII对象被赋值时,会发生声明事情?
禁止复制
对底层资源祭出“引用计数法”
复制底部资源(深度拷贝)
转移底部资源的拥有权
请记住:
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的行为。
普遍而常见的RAII类的copying行为是:禁止copying,施行引用计数法。不过其他行为也都可能被实现。
条款15:在资源管理类中提供对原始资源的访问
许多APIs直接指涉资源,需要直接访问原始资源。这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源。有两种做法可以达成目标:显示转换和隐式转换。
tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件)。就像所有智能指针一样, tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针。
显式转换:
条款13导入一个观念,使用智能指针入auto_ptr或tr1::shared_ptr保持factory函数如createInvestement的调用结果:
std::tr1::shared_ptr<Investment> pInv(createInvestment());
假如希望以某个函数处理Investment对象,像这样:
int dayHeld(const Investment* pi);
你想这么调用它:
int days=dayHeld(pInv); //错误
通不过编译,因为dayHeld需要的是Investment*指针,传给它的却是个类型为tr1::shared_ptr<Investment>
的对象。
auto_ptr和tr1::shared_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件)。同时它们也重载了指针取值操作符(operator->和operator*),允许隐式转换至底部原始指针。
隐式转换:
class Font{ public: explicit Font(FontHandle fn) :f(fn){} .... FontHandle get() const{ return f; } operator FontHandle() const{ //隐式转换 return f; } ... ~Font(){ realseFont(f); } private: FontHandle f; };
请记住:
APIs往往需要取得RAII的原始资源,所以每一个RAIIclass应该提供一个“取得其所管理之资源”的方法。
对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new和delete时采取相同形式
当你使用new动态生成一个对象,有两件事发生:第一,内存被分配。第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存被释放。
单一对象的内存布局一般而言不同于数组的内存布局。数组所用的内存通常还包括“数组大小“的记录,以便delete知道需要调用多少次析构函数。
string* stringpPtr1=new std::string; string* stringpPtr2=new std::string[100]; ... delete stringpPtr1; delete [] stringpPtr2;
尽量不要对数组形式做typedef动作。
请记住:
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
条款17:以独立语句将newed对象置入智能指针
int priority(); void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
采用如下形式调用processWidget
processWidget(std::tr1::shared_ptr<widget> pw(new widget), priority());
虽然在此使用“对象管理式资源”,上述调用可能出现资源泄露。因为在函数中,参数的调用顺序会因为编译器的不同而不同,例如上面的顺序可能是:
执行”new widget”
调用priority
调用tr1::shared_ptr构造函数
这样可能出现的问题就是当new widget成功后,如果priority()函数调用导致异常,new widget返回的指针将会遗失,因为new widget未能放入到智能指针中,导致内存泄漏。
避免这类问题办法很简单:使用分离语句:
std::tr1::shared_ptr<Widget> pw(new Widget); //在单独语句内以智能指存储newd所得对象 processWidget(pw, priority()); //这个调用动作绝不至于造成泄漏
请记住:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。