有多种做法可以记录时间,因此,设计一个TimeKeeper基类和一些派生类作为不同的计时方法,相当合情合理:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock : public TimeKeeper {...};
class WaterClock : public TimeKeeper {...};
class WristWatch : public TimeKeeper {...};
许多客户只想在程序中使用时间,不想操心如何计算时间等细节,这时我们可以设factory函数,返回指针的一个计时对象。factory函数会返回一个基类指针,指向新生成的派生类对象:
TimeKeeper* getTimeKeeper();
为遵守factory函数的规范,被getTimeKeeper()返回的对象必须位于heap。因此为了避免泄露内存和其他资源,将factory函数返回的每一个对象适当地delete掉很重要:
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;
这引来一个灾难,因为C++指出,当派生类对象经由一个基类指针被删除,而该基类带着一个non-trivial析构函数,其结果没有定义——实际执行时通常发生的是对象的派生部分没有被销毁,于是造成一个局部销毁对象。
消除这个问题的做法很简单:给基类一个虚析构函数。此后删除派生类对象就会如你想要的那般。它会销毁整个对象,包括所有派生类成分:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;
无端地将所有类的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。只有当类内至少有一个virtual函数,才为它声明virtual析构函数。
即使类不完全带virtual函数,被non-trivial析构函数问题给咬伤还是有可能的。举个例子,标准string不含任何virtual函数,但有时程序员会错误地把它当作基类:
class SpecialString : public std::string {...};
乍看似乎无害,但如果你在程序某处无意间将一个SpecialString指针转换为要给string指针,然后将string指针delete掉,会导致未定义行为:
SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss; // SpecialString* => std::string*
...
delete ps; // UB
有时令class带一个纯虚析构函数,可能颇为便利。纯虚函数会导致抽象类——也就是不能被实例化地类。可以为你希望它成为抽象的那个类声明一个纯虚析构函数,可以为一个没有其他纯虚函数的类成为抽象类。下面是个例子:
class AWOV { // abbr. abstract w/o virtual
public:
virtual ~AWOV() = 0;
};
你必须为这个纯虚析构提供一份定义:
AWOV::~AWOV() {}
析构函数的运作方式是,最深层派生的那个类的析构函数最先被调用,然后是其每一个基类的析构函数被调用。编译器会在AWOV的派生类析构函数中创建一个对 ~AWOV()
的调用,所以你必须为这个函数提供定义。
“给一个基类一个虚析构函数”这个规则只适用于多态基类。并非所有的基类的设计目的都是为了多态用途,不需要虚析构函数。
请记住
- 多态的基类应该声明一个virtual析构函数。如果类带有任何virtual函数,它就应该拥有一个virtual析构函数。
- 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。