本文主要概括一下Effective C++一书中的构造/析构/赋值运算章节的内容,并且做简要的应用分析。
1) 条款05:了解C++默默编写并调用那些函数
如果用户没声明empty class(空类)是个empty class,C++处理过,编译器就会为它声明(编译器版本的)一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline(条款30)。比如当你写下class Empty{ };时,经过C++处理后,就好像你写下的是:
class Empty{
public:
Empty(){...} //default 构造函数
Empty(constEmpty& rhs) {...} //copy构造函数
~Empty( ){...} //析构函数
Empty&operator =(const Empty& rhs){...} //copy assignment操作符
};
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
2)条款06:若不想使用编辑器自动生成的函数,就该明确拒绝
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。
3)条款07:为多态基类声明virtual析构函数
polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性 (polymorphically),就不该声明virtual析构函数。
4)条款08:别让异常逃离析构函数
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。比较好的策略就是重新设计DBConn接口,使其客户有机会对可能出现的问题。例如DBConn自己可以提供一个close函数,因而赋予客户一个机会得以处理
“因该操作而发生的异常”。DBConn也可以追踪其所管理之DBConnection是否已被关闭,并在答案为否的情况下由其析构函数关闭之。这可防止遗失数据库连接。然而如果DBConnection析构函数调用close失败,我们又将退回“强迫结束程序”或“吞下异常”的老路。看代码:
class DBConn{ //这个class 用来管理DBConnection对象
public:
...
voidclose() { //供客户使用的新函数
db.close();
closed =true;
}
~DBConn(){
if(!closed){
try{ //关闭链接
db.close();
}
catch(...){//如果关闭动作失败,记录下来并结束程序或者吞下异常。
....
}
}
}
private:
DBConnectiondb;
bool closed;
};
这个时候可以把调用close的责任从DBConn析构函数手上移到DBConn客户手上,尽管DBConn析构函数仍内含一个“双保险”调用。这样并不会为他们带来负担,因为他们有机会第一手处理问题。
下面说一下如何结束程序和吞下异常。
a) 在close抛出异常时,就结束程序,通常调用abort完成。这样可以确保异常从析构函数传播出去,导致不明确的行为发生。
try{ //关闭链接
db.close();
}
catch(...){//如果关闭动作失败,记录下来并结束程序或者吞下异常。
....//制作运转记录,记下close的调用失败
std::abort();
}
b) 吞下因调用close而发生的异常。这样使得程序在遭遇并忽略一个错误后可以继续可靠地运行。
try{ //关闭链接
db.close();
}
catch(...){//如果关闭动作失败,记录下来并结束程序或者吞下异常。
....//制作运转记录,记下close的调用失败
}
5)条款09:绝不在构造或者析构过程中调用virtual函数
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class
(比起当前执行构造函数和析构函数的那层)。那些derivedclass的函数几乎必然却用local成员变量,而成员变量处于未定义状态。直接导致不明确的行为和莫名奇妙的问题。
6) 条款10:令oprator=返回一个reference to *this
关于赋值,有趣的是你可以把它们写成连锁形式:
int x, y, z;
x = y = z = 15;//赋值连锁形式
同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:
x=(y=(z=15));
这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x。
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:
class Widget {
public:
...
Widget&operator =(const Widget& rhs)
{
...
return* this; //返回左侧对象
}
};
这个协议,并无强制性,不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,如+=,-=, *=,对于函数也是用即使此操作符的参数类型不符协定。但是这个协议被所有的内置类型和标准程序库提供的类型如string, vector, complex等共同遵守。建议直接采用。
7)条款11:在operator=中处理“自我赋值”
确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap 。确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
自我赋值发生在对象被赋值给自己时。处理时,传统做法时做一个证同测试:
Widget&operator =(const Widget& rhs)
{
if(this ==&rhs) return *this;//如果是自我赋值,就不做任何事情
...
return*this;
}
copy-and-swap技术,后续再深入。
8)条款12:赋值对象时勿忘其每一个成分
Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。