[C++] virtual析构函数

有许多种做法可以记录时间,
因此,设计一个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派生类的动态分配对象
TimeKeeper* getTimeKeeper();

为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。
因此为了避免泄漏内存和其他资源,将factory函数返回的每一个对象,适当的delete掉很重要。

// 从TimeKeeper继承体系获得一个动态分配对象
TimeKeeper* ptk = getTimeKeeper();
...

// 释放它,避免资源泄漏
delete ptk;

1. 局部销毁

虽然倚赖客户执行delete动作,基本上便带有某种错误倾向,
factory函数接口也该修改以便预发常见之客户错误,
但这些在此都是次要的,因为纵使客户把每一件事都做对了,仍然没办法知道程序如何行动。

为题出在getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),
而那个对象却经由一个base class指针(例如一个TimeKeeper*指针被删除),
而目前的base class(TimeKeeper)有个non-virtual析构函数。

这是一个引来灾难的秘诀,因为C++明确指出,
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义。
实际执行时,通常发生的是,对象的derived成分没有被销毁。

如果getTimeKeeper返回指针指向一个AtomicClock对象,
在其内的AtomicClock成分(也就是声明于AtomicClock class内的成员变量)很可能没被销毁。
AtomicClock的析构函数也未能执行起来。
然而其base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。
这可是形成资源泄露,败坏之数据结构,在调试器上浪费许多时间的绝佳途径喔。

消除这个问题的做法很简单,给base class一个virtual析构函数。
此后删除derived class对象就会如你想要的那般。
是的,它会销毁整个对象,包括所有的derived class成分。

TimeKeeper这样的base class除了析构函数之外,通常还有其他的virtual函数,
因为virtual函数的目的,是允许derived class的实现得以客制化。
例如,TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的derived class中有不同的实现码。
任何class只要带有virtual函数,都几乎确定应该也有一个virtual析构函数。

2. vptr指针

如果class不包含virtual函数,通常表示它并不意图用作一个base class。
当class不企图被当做base class,令其析构函数为virtual往往是个馊主意。
欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。
这份信息通常是由一个所谓vptr(virtual table pointer)指针指出。
vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。

每一个带有virtual函数的class都有一个相应的vtbl,当对象调用某一个virtual函数,
实际被调用的函数,取决于该对象的vptr所指的那个vtbl,编译器在其中寻找适当的函数指针。

virtual函数的实现细节不重要,重要的是如果class内含virtual函数,其对象的体积会增加。
vptr指针,在32-bit计算机体系结构中,将多占用32bits,在64-bit计算机体系结构中,会多占用64-bits。
因此,无端的将所有的class的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。
许多人的心得是,只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

3. non-virtual析构函数

即使class完全不带virtual函数,被“non-virtual析构函数问题”给咬伤还是有可能的。
举个例子,标准string不含任何virtual函数,但有时候程序员会错误的把它当做base class:

// 馊主意,std::string有个non-virtual析构函数
class SpecialString: public std::string{
    ...
};

乍看似乎无害,但如果你在程序任意某处无意间将一个pointer to SpecialString转换成一个pointer to string
然后将转换所得的那个string指针delete掉,你立刻被流放到“行为不明确”的恶地上。

SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...

// SpecialString* => std::string*
ps = pss;
...

// 未有定义,现实中*ps的SpecialString资源会泄露,
// 因为SpecialString的析构函数没被调用
delete ps;

相同的分析适用于任何不带virtual析构函数的class,包括所有STL容器,如vectorlistsettr1::unordered_map等等。
如果你曾经企图继承一个标准容器或任何其他“带有non-virtual析构函数”的class,拒绝诱惑吧。
(很不幸C++没有提供类似Java的final class或C#的sealed class那样的“禁止派生”机制)

4. pure virtual析构函数

有时候,令class带一个pure virtual析构函数,可能颇为便利。
pure virtual函数,导致abstract(抽象) class,也就是不能被实体化(instantiated)的class。
也就是说,你不能为那种类型创建对象。

class AMOV{
public:

    // 声明为pure virtual析构函数
    virtual ~AMOV() = 0;
};

这个class有一个pure virtual函数,所以它是个抽象class,
又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。

析构函数的运作方式是,最深层(most derived)的那个class其析构函数最先被调用,
然后是其每一个base class的析构函数被调用。
编译器会在AMOV的derived class的析构函数中创建一个对~AMOV的调用动作,
所以,你必须为这个函数提供一份定义,如果不这样做,连接器会发出抱怨。

// pure virtual析构函数的定义
AMOV::~AMOV() { }

总结

“给base class一个virtual析构函数”,这个规则只适用于polymorphic(带多态性质)的base class身上。
这种base class的设计目的是为了用来“通过base class接口处理derived class对象”。
TimeKeeper就是一个polymorphic base class,
因为我们希望处理AtomicClockWaterClock对象,纵使我们只有TimeKeeper指针指向它们。

并非所有的base class的设计目的都是为了多态用途,
例如标准string和STL容器,都不被设计作为base class使用,更别提多态了。
某些class的设计目的是作为base class使用,但不是为了多态用途,
它们并非设计用来“经由base class接口处置derived class对象”,因此,它们不需要virtual析构函数。


Effective C++ - P40

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容