结论:
- 多态性质的基类声明一个虚析构函数,如果class带有任何虚函数,它就应该有一个virtual析构函数。
- class的设计目的如果不是作为一个基类使用,或不是为了具备多态,就不该声明虚析构函数。
案例一:
设计一个TimeKeeper base class和derived class作为不同的计时方法,如下代码:
class TimeKeeper
{
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock : public TimeKeeper {...}; // 原子钟
class WaterClock : public TimeKeeper {...}; // 水钟
class WristWatch : public TimeKeeper {...}; // 腕表
许多客户指向在程序中使用时间,不想操心时间如何计算细节,这时候我们可以设计factory(工厂)函数,返回指针指向一个计时对象。Factory函数会“返回一个base class指针,指向新生成的derived class对象”:
TimeKeeper* getTimeKeeper(); // 返回一个指针,指向一个TimerKeeper派生类的动态分配对象。
为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。因此为了避免泄漏内存和其它资源,将factory函数返回的每一个对象释放地delete掉很重要:
TimeKeeper* ptk = getTimeKeeper(); // 从TimeKeeper继承体系获得一个动态分配对象
... // 运用它
delete ptk; // 释放它,避免资源泄漏
问题:
getTimeKeeper返回的指针指向一个derived class对象,而那个对象却经由一个base class指针被删除,而目前base class有个non-virtual析构函数。这将引来灾难。因此C++明确指出,当derived class对象经由一个base class指针被删除,而改base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没有被销毁。
解决方法:
给base class一个虚析构函数。
class TimeKeeper
{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper* ptk = getTimeKeeper();
,,,
delete ptk; // 现在行为正确
topic1: 不含virtual函数的class,通常不令析构函数为虚函数
如果class不含virtual函数,通常表示它并不做一个base class。当class不企图被当作base class,令其析构函数为virtual往往是一个馊主意。主要是内存考虑。
案例1
下面是一个用来表示而为空间坐标的class:
class Point
{
public;
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果int占用32 bits,那么Point对象占用64 bits。如果Point的析构函数为virtual,则会变得不同。
欲实现虚函数,对象中必须存在vptr(virtual table pointer),vptr主要用来在运行期间决定哪一个虚函数被调用。vptr指向一个由函数指针构成的数组,成为vtbl(virtual table);每一个带有虚函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。
virtual函数的实现细节不重要。重要的是,如果Point Class内含virtual函数,其对象的体积会增加:在32-bite计算机体系结构中将占用64~96 bits(2个int加上vptr);在64-bit计算机体系结构中可能占64~128bits,因为指针在这样的计算机结构中占64bits。因此,为Point添加一个vptr会增加其对象大小达50%~100%。Point不再能塞入一个64-bit缓存起,而C++的Point对象也不再和其它语言(如C)内的相同声明有一样的机构(因为其它语言的对应物中并没有vptr),因此也就不再可能把它传递至其它语言所写的函数,除非你明确补偿vptr——那属于实现细节,也因此不再具有移植性。
因此,无端地将所有class的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才声明virtual析构函数。、
topic2: 不要选择non-virtual析构函数的class作为基类
即使class完全不带virtual函数,被“non-virtual析构函数问题”给咬伤还是有可能的。举个例子,标准string不含任何virtual函数,但有时候程序员会错误地把它当作base class。
class SpecialString : public std::string // 馊主意!std::string有个
{...}; // non-virtual析构函数
乍看无害,但如果你在程序任意某处无意间将一个pointer-to-specialString转换为一个pointer-to-string,然后转换所得的那个string指针delete掉,你立刻被流放到“行为不明确”的恶地上:
SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss;
...
delete ps; // 未定义行为!现实中*ps的SpecialString资源会泄漏,
// 因为SpecialString析构函数没被调用
topic3: virtual returnVlaue XXX() = 0;纯虚函数的使用场景
纯虚函数定义格式
virtual 返回值类型 函数名(函数参数)=0;
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。