条款7, 为多态基类声明virtual析构函数
Car *c = new YellowCar();
delete c;
c指向了一个YellowCar的对象,但c是一个Car指针,如果Car的析构函数不为虚函数,那么delete时,只会调用Car的析构函数。如果YellowCar中有动态申请的内存,这块的释放逻辑通常在它的析构函数来完成,那么delete时,无法调用到YellowCar的析构,所以就造成了内存泄漏。
不过要注意,如果没有用到多态,也就是说基类中没有虚函数,而把基类的析构函数声明为虚,这种做法是不好的。一来,虚函数需要通过虚函数表指针访问,虚表指针要占对象的空间。二来,类型转换上会带来不确定性。所以,没必要画蛇添足。
条款8,别让异常逃离析构函数
反例如下,当 doSomething 函数执行完毕的时候,会开始析构容器 v 中存放的各个 Widget 对象。然而,该对象析构函数可能会吐出异常。在这种情况下,将会吐出容器中对象个数的异常,这就会导致程序运行的不确定性。
class Widget {
public:
//......
~Widget() { // 假定这个析构函数可能会吐出异常
//......
}
//......
};
void doSomething()
{
//......
std::vector<Widget> v;
//......
}
正确使用的方法,在析构中处理异常,而不是让它抛出
// 修改后的DBConn类实现
class DBConn {
public:
//......
void close() { // 要求用户自己关闭数据库对象
db.close();
closed = true;
}
//......
~DBConn() {
if (!closed) { // 如果用户忘记了这么做,就采用 try catch 机制吞下异常。
try {
db.close();
}
catch (...) {
// 记录此次 close 失败
//......
}
}
}
//......
private:
//......
DBConnection db;
bool closed; // 增设此变量用以判断用户是否已经自行调用 close(),用户也可根据此变量判断 close() 是否顺利执行并作出相应的异常处理。
//......
};
条款9,不在构造和析构函数中调用虚函数
class Transaction {// 所有交易的基类
public:
Transaction();
virtual void logTransaction() const = 0;//建立依赖于具体交易类型的登录项
...
};
Transaction::Transaction() //实现基类的构造函数
{
...
logTransaction(); //最后,登录该交易
}
class BuyTransaction: public Transaction {
// 派生类
public:
virtual void logTransaction() const; //怎样实现这种类型交易的登录?
...
};
class SellTransaction: public Transaction {
//派生类
public:
virtual void logTransaction() const; //怎样实现这种类型交易的登录?
...
};
在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。通俗的讲,在基类的构造过程中,虚函数并没有被"构造"。
因为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。如果在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为和彻夜的代码调试。
一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。