条款七:为多态基类声明virtual析构函数
class TimeKeeper
{
public: TimeKeeper();
~TimeKeeper();
};
class AtomicClock:public TimeKeeper{};//原子钟
class WaterClock:public TimeKeeper{};//水钟
class WristClock:public TimeKeeper{};//腕钟
设计一个time的基类和多种计时方法,许多客户只想使用时间,不关系如何实现时间的细节,我们可以设置一个factor函数
TimeKeeper *getTimeKeeper();
根据factor的规则,geTimeKeeper返回的对象必须位于堆区,因此为了避免内存泄漏,我们需要对其进行释放;
TimeKeeper *ptk=getTimeKeeper();
...
delete ptk;
然而纵使客户把每一件事都做对了,仍然无法知道程序如何行动。
问题出在返回的指针指向派生类对象,但那个对象却被基类指针delete,而目前的基类析构函数是非虚函数。
c++明确指出,当派生类对象经由基类指针删除,而基类指针是非虚析构函数结果通常是对象派生类部分未被delete,并且派生类对象的析构函数未能执行,而基类的部分被销毁,所以会造成一种诡异的局部销毁对象,这种情况会造成资源泄露、败坏数据结构等一系列问题。
解决方法是将基类析构函数声明为虚函数,之后删除派生类对象就会如我们预期的那般销毁整个对象(包括基类的那部分)。
class TimeKeeper
{
public: TimeKeeper();
virtual ~TimeKeeper();
};
class AtomicClock:public TimeKeeper{};//原子钟
class WaterClock:public TimeKeeper{};//水钟
class WristClock:public TimeKeeper{};//腕钟
TimeKeeper *ptk=getTimeKeeper();
...
delete ptk;
像上述这样是正确的行为。
任何class带有虚函数几乎确定也有一个虚析构函数
如何一个类不含有虚函数,那么它通常不会作为基类被继承,那么声明其析构函数为虚函数是不妥的。
想要实现虚函数,其对象通常需要包含虚函数指针,虚函数指针指向的是由函数指针组成的虚函数表,每一个带有虚函数的类都由一个虚函数表,当对象调用某一个虚函数,实际上调用的是虚函数指针指向的虚函数表,编译器会在其中找合适的函数。
然而一个包含虚函数的类其体积会增加,因为其会增加一个额外的虚函数指针从而增加额外的开销。
因此只有当类至少包含一个虚函数时,才将其析构函数声明为虚函数。
即使一个类没有虚函数,但也有可能面临非虚析构函数的问题
class SpecialString:public string
{ ...};
SpecialString *pss=new SpecialString("hello");
string *ps;
ps=pss;
delete ps;
在该情况下,SpecialString的资源会被泄露,因为其析构函数没有调用。
相似的类型适用于所有不带虚析构函数的类,包括STL所有容器。因此,不要继承标准容器或者任何不带虚析构函数的类。
有时候声明一个纯虚函数可能会带来一些便利,然而有的时候当你希望有一个抽象类,但却没有可用的纯虚函数。我们可以将析构函数声明为纯虚函数。但需要注意的是,我们必须为这个纯虚函数提供定义。
class AWOV
{
public:
~AWOV ()=0;
};
AWOV::~AWOV(){};
析构函数的调用顺序的最深处派生的派生类析构函数先调用,然后是其基类,编译器会在AWOV类的派生类的析构函数中调用~AWOV,所以必须提供定义,不然会报错。
给基类一个虚析构函数,这个规则只适用于带多态性质的基类身上,然而并非所有的基类设计目的都是为了多态,如string和STL容器。这样的类如条款六提到的uncopy类等。
总结:
- 多态性质的基类应该声明一个虚析构函数、如果类带有任何虚函数、他就应该有一个虚析构函数
- 类的设计如果不是作为基类使用或者不是为了具有多态性,那么就不应该具有虚析构函数。
条款八:别让异常逃离析构函数
c++并不禁止析构函数抛出异常,但它不建议你这么做。
class Widget
{
public:
...
~Widget(){...}
};
void dosomething()
{
vector<Widget> v;
}//v在此处被销毁
当v被销毁时,它负责销毁其内所有的Widget,假设v有是个Widget,但在其析构第一个期间抛出了异常,其他九个还是应该被销毁。假设在次期间,第二个Widget也抛出异常。如果有两个同时作用的异常,程序不是停止运行就是导致不明确的行为,本例中会导致不明确行为。使用标准库的其他容器甚至array也会出现相同的情况,因此,c++不喜欢析构函数抛出异常。
但如果你的析构函数必须执行一个可能中断的函数怎么办?
class DBconn
{
public:~DBconn()
{
db.close();
}
private:
DBConnect db;
};
如果close调用异常,那么析构函数就会抛出异常。
两个方法可以避免该问题
方法一,close异常就结束程序,通常使用abort函数实现。
class DBconn
{
public:~DBconn()
{
try {db.close();}
catch {... std::abort();}
}
private:
DBConnect db;
};
方法二:吞下异常
class DBconn
{
public:~DBconn()
{
try {db.close();}
catch {... }
}
private:
DBConnect db;
};
一般来说,将异常吞掉时一个不好的习惯,但有时候也比不明确行为或者草草结束程序好。
问题在于这两种方法都无法对close抛出异常作出反应
一个较佳策略是重新设置DBconn结口,使客户端有可能对可能出现的问题作出反应。例如壳子提供一个close函数赋予客户一个机会处理异常。
class DBconn
{
public:
void close() //供用户使用
{
db.close();
closed=true;
}
~DBconn()
{
if(!closed)
{
try {db.close();}
}
catch {... }
}
private:
DBConnect db;
bool closed;
};
可以使得用户调用close,给用户机会来处理异常。若用户没有处理,则回退至上述吞下异常的老路。如果真的抛出异常被吞下,用户也没有怨言,因为已经给了用户自己处理的机会,而用户选择了放弃。
总结:
- 析构函数绝对不要吐出异常。如果被析构函数调用的函数可能抛出异常,那么析构函数应该扑获任何一场,然后吞下他们或结束程序。
- 如果客户需要对抛出的异常作出反应,那么类应该提供一个函数执行该操作。
条款九:绝不再构造或析构过程中调用虚函数
重点:不应该在构造或者析构函数期间调用虚函数,因为其不会带来你想要的结果。
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;//记录交易
...
};
//接下来执行
BuyTransaction b;
该代码会发生什么事呢?
有一个BuyTransaction构造函数被调用,但在其之前,应该是Transaction构造函数更早调用。Transaction构造函数调用virtual 函数logTransaction,这时候logTransaction是Transaction内的版本,不是BuyTransaction的版本,即使目前对象是BuyTransaction。所以,基类构造函数期间运行的虚函数不会下调到派生类。即在基类构造期间,虚函数不是虚函数。
有一个好理由,即基类构造函数执行时间早于派生类,基类构造函数执行期间派生类成员变量还尚未初始化,而如果虚函数真的下发至派生类,而那些成员尚未初始化,那么会造成非常严重的结果。
还有更根本的原因:派生类对象的基类构造期间,对象类型是基类而不是派生类,不只虚函数会被编译器解析至基类,若使用运行期间信息(如dynamic_cast和typeid),也会把对象视为基类类型。本例当中,Transaction构造函数打算初始化BuyTransaction对象内的基类成分时,对象类型还是Transaction。
相同的道理也适用于析构函数,一旦派生类的析构函数开始执行,对象的派生类成员变量便呈现未定义值,进入基类析构函数之后就变为基类对象,那么虚函数和dynamic_cast等也会这样看待它。
但是侦测构造函数或析构函数在运行期间是否调用虚函数不总是像上述例子这样轻松。若Transaction有多个构造函数,并且其都需要执行某些相同的工作,那么可以把共同的代码放入一个init函数中。
class Transaction//所有交易的基类
{public:
Transaction(){init();};
virtual void logTransaction()const=0; //做一份因类型不同而不同的日志记录
private:
void init()
{
logTransaction();
}
};
这段代码与稍早版本相同,但它比较隐藏且暗中危害,因为它不会引起编译器警告。
由于logTransaction是Transaction内的纯虚函数,当它被调用时大多数系统会终止程序。然而若logTransaction是一个正常的虚函数,那么该版本就会被调用,程序会正常运行,留下我们百思不得其解为什么派生类对象会调用错误版本的logTransaction。唯一能够避免此问题的做法是:确保你的析构函数和构造函数都没有调用虚函数,并且他们所调用的函数也应该如此。那该如何确保Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?
其他方案可以解决该问题,一种方法是将logTransaction改为非虚函数,要求派生类构造函数传递必要信息给Transaction构造函数,而后那个构造函数可以安全调用logTransaction。
class Transaction//所有交易的基类
{public:
explicit Transaction(const string &loginfo);
void logTransaction()const; //做一份因类型不同而不同的日志记录
};
Transaction::Transaction(const string &loginfo)
{
...
logTransaction(); //记录这笔交易
}
class BuyTransaction:public Transaction
{public:
BuyTransaction(param):Transaction(createlogstring(param))//将log信息传递给
{} //Transaction构造函数
private:
static string createlogstring(param);
};
class SellTransaction:public Transaction
{public:
virtual void logTransaction()const;//记录交易
...
};
无法使用虚函数从基类乡下调用,构造期间可以将派生类必要信息传递给基类构造函数。
注意本例中createlogstring(param)的运用,比起初始列内传递数据,利用辅助函数创建一个值传递给基类往往比较方便,且可读。令此函数为静态函数,也就不可能指向构造期间未初始化的成员变量。
总结:
- 在构造析构函数期间不要调用虚函数,因为这类调用不下降至派生类