2 构造/析构/赋值运算
条款05:了解 C++ 默默编写并调用哪些函数
如果自己没声明,编译器就会为类声明(编译器版本的)一个copy构造函数,一个copy assignment操作符和一个析构函数。此外如果没有声明任何构造函数,编译器也会声明一个default构造函数。所有这些函数都是public且inline的。
惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
default构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数。
至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
如果一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。
编译器生成的copy assignment操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。
** note: **
编译器可以暗自为类创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。
由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。通过这个小“伎俩”可以阻止人们在外部调用它,但是类中的成员函数和友元函数还是可以调用private函数。解决方法可能是在一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。
不允许用户进行对象的拷贝。一般编译器会提供默认拷贝,可将相应的成员函数声明为private。但这时有个问题,member(成员)函数和friend(友元)函数仍然可以调用。
在不想实现的函数中不写函数参数的名称。当客户企图copy,编译器会阻挠他。如果不慎在member或friend函数内copy,连接器会出问题。
class HomeForSale{
public:
...
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
- 将连接器错误移至编译期,更早地发现错误往往更好。定义一个Uncopyable的基类,其它类继承该类,当执行拷贝时,要调用基类拷贝构造函数,就会出现问题。
class Uncopyable{
protected:
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
// 继承Uncopyable
class HomeForSale: private Uncopyable{
... ...
}
** note: **
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的基类也是一种做法。
条款07:为多态基类声明 virtual 析构函数
当基类的指针指向派生类的对象的时候,当我们使用完,对其调用delete的时候,其结果将是未有定义——基类成分通常会被销毁,而派生类的成分可能还留在堆里。这会形成资源泄漏,败坏之数据结构,在调试器上消费许多时间。
消除这个问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会销毁整个对象,包括所有derived class成分。
任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。 因此,只有当class内含至少一个virtual函数,才为它声明virtual析构函数。
STL容器都不带virtual析构函数,所以最好别派生它们。
某些classes的设计目的是作为基类使用,但不是为了多态用途,如条款06的Uncopyable,因此它们不需要virtual析构函数。
** note: **
- 带有多态性质的基类应该声明一个virtual析构函数。如果一个类带有任何virtual函数,它就应该拥有一个virtual析构函数。
- 一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。
class DBConnection{
public:
static DBConnection create();
void close();
};
class DBManager{
public:
~DBManager(){
db.close(); //析构函数关闭数据库连接
}
private:
DBConnection db;
};
//调用析构函数时可能会发生异常
DBManager dbM(DBConnection::create());
如果析构函数调用的函数抛出异常:
- 如果抛出异常,就结束程序。强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。通常通过调用about完成。
DBManager::~DBManager(){
try { db.close(); }
catch(...){
// 可以记录错误后退出程序
std::abort();
}
}
- 吞下发生的异常。捕获异常,但什么也不做。 虽然吞下异常是个坏主意,但吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。
DBManager::~DBManager(){
try { db.close(); }
catch(...){
//可以记录错误后退出程序
}
但是,两者都无法对“导致close抛出异常”的情况做出反应。即使析构函数捕获到异常,客户也无法处理异常,客户需要对某个函数运行期间抛出的异常进行反应。因此,class应该提供一个普通函数来执行该操作。
class DBManager(){
public:
void close(){
db.close();
closed = true;
}
~DBManager(){
if (!closed){
try { db.close(); }
catch(...) {
//错误日志...
}
}
}
private:
bool closed;
DBConnection db;
};
这里面加了一个close函数,客户可以自己调用close函数,当发生异常时,进行异常处理,析构函数再重新关闭一次(如果这次仍然异常,就和上面的结果一样了)。如果客户没有调用close函数,则可以在析构函数中自动调用。所以,在写程序时,一定要将会发生异常的函数作为一个普通函数,这样可以提供更多的选择。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
** note: **
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。