现在的位置: 首页 > 综合 > 正文

【读书笔记】Effective C++-2 构造/析构/赋值运算(之四)

2014年02月04日 ⁄ 综合 ⁄ 共 3382字 ⁄ 字号 评论关闭

Effective C++读书笔记

--By Nathan.Yu 2007-11-24--

2 构造/析构/赋值运算(之四)

条款08:别让异常逃离析构函数

C++并不禁止析构函数吐出异常,但它不鼓励你这样做!

理由:

       设:std::vector<Widget> v;

假设v中有多个Widget,在销毁v的过程中,当有2个以上的Widget在销毁的时候抛出异常,对C++而言就太多了。在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。

       只要是在析构函数吐出异常,即使并非使用容器,程序也可能过早结束或出现不明确行为。

 

       如果程序在析构函数中捕获异常,“强迫结束程序,即调用abort”是个合理的选项。可以抢先制“不明确行为”于死地。

       另,将异常吞没是个坏主意。因为它压制了“某些动作失败”的重要信息。为了让这成为一个可行的方案,程序必须确保能够继续可靠地执行,即使在遭遇并忽略一个错误之后。

       更好地做法是,赋予客户一个处理异常的机会。如果某个操作必须在析构函数中使用,它又可能抛出异常,则将这个操作作为一个公共的接口,允许客户手动调用,让客户自处理异常。在析构函数中,判断客户是否调用过该函数,如若没有,则再次调用,并对可能抛出的异常进行处理。

       总之,如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。

 

请记住:

1、            
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它或结束程序。

2、 
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作

 

【备注】参见More Effective C++
条款十一:禁止异常信息(exceptions)传递到析构函数外

 

条款09:绝不在构造和析构过程中调用virtual函数

<注意>这是C++JavaC#不同的一个地方

1Base class构造期间virtual函数绝不会下降到derived
classes
阶层。

2、在base class构造期间,virtual函数不是virtual函数。

3、在derived class对象的base
class
构造期间,对象的类型是base class而不是derived class。因此,不止vritual函数会被编译器解析为base
class
,若使用运行期类型信息(如dynamic_casttypeid),也会把对象视为base
class
类型。

4、对象在derived class构造函数开始执行前不会成为一个derived class
对象。

 

相同道理也适用于析构函数。

 

解决方案:改用non-virtual函数,并要求derived class构造函数传递必要信息给base class构造函数,base
class
构造函数中调用non-virtual,该函数根据不同的信息处理不同的derived class

 

请记住:

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived
class

 

 

条款10:令operator=返回一个reference to *this

协议:为了实现x=y=z=15之类的“连锁赋值”,赋值操作符必须返回一个引用指向操作符的左侧实参。

这个协议适合于所有赋值相关运算operator op=)。

 

请记住:

令赋值(assignment)操作符返回一个reference
to *this

 

 

条款11:在operator=中处理“自我赋值”

什么情况发生“自我赋值”?

情况1Widget w;      
w = w;

情况2a[i] = a[j]; //潜在的自我赋值,当i
== j

情况3*px = *py; //pxpy恰巧指向同一个对象。

 

详细分析operator=时可能出现的情况:

假设类:

class Bitmap{……};

class Widget{

……

private:

       Bitmap* pb;

};

operator=的不同实现版本:

 

版本1一份不安全的实现版本,不具“自我赋值安全性”,不具“异常安全性”

Widget&

Widget::operator=(const Widget& rhs)

{

       delete pb;

       pb = new Bitmap(*rhs.pb);

       return *this;

}

这里的自我赋值是,*thisrhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap了,也销毁rhspb。那么*this将持有一个已被删除的对象。

 

版本2使用“证同测试(Identity
test)
”阻止版本1中的错误,达到“自我赋值”的检测目的,具“自我赋值安全性”,不具“异常安全性”

Widget&

Widget::operator=(const Widget& rhs)

{

       if(this == &rhs) return *this;

       delete pb;

       pb = new Bitmap(*rhs.pb);

       return *this;

}

这里,new Bitmap可能导致异常(不论是因为分配内存不足或因为Bitmap的拷贝构造抛出异常),Widget会持有一个指针指向一块被删除的Bitmap

 

版本3忽视“自我赋值”,把焦点放在实现“异常安全性上”,因为让operator=具备“异常安全”往往自动获得“自我赋值安全”的回报。

<注意>“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码”,例如:

Widget&

Widget::operator=(const Widget& rhs)

{

       Bitmap* pOrig = pb; //记住原先的pb

       pb = new Bitmap(*rhs.pb); //
pb指向*pb的一个副本

       delete pOrig; //删除原先的pb

       return *this;

}

它或许不是处理“自我赋值”的最高效办法,但它行得通。

 

版本4copy
and swap
技术,处理“自我赋值”的一个简单而通用的技术,并且是“异常安全的”(该版本的详细讨论可参考Exceptional C++

Widget&

Widget::operator=(const Widget& rhs)

{

       Widget temp(rhs);

       swap(temp);

       return *this;

}

 

请记住

1、            
确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap

2、            
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正常。

 

 

条款12:复制对象时勿忘其每一个成分

如果你为class添加一个成员变量,你必须同时修改copying函数(copy构造,copy赋值),也需要修改所有其他构造函数以及任何非标准形式的operator=

 

任何时候只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心的复制其base
class
成分。那些成分往往是private的,所以你无法直接访问它们,你应该让derived classcopying函数调用相应的base
class
函数。

 

能否令某个copying函数调用另一个copying函数?不能!

copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。

copy构造函数调用copy赋值操作符同样无意义。构造函数用来初始化新对象,而赋值操作符只施加于已初始化对象身上。

使用private的函数init()来消除重复代码。

 

请记住:

1、        
copying函数应该确保复制“对象内的所有成员变量”及“所有base
class
成分”。

2、        
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

 

 

抱歉!评论已关闭.