Effective C++——条款7(第2章)

条款07:    为多态基类声明 virtual 析构函数

Declare destructors virtual in polymorphic base classes

设计以下时间基类TimeKeeper:

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();    // 返回一个指针,指向TimeKeeper派生类的动态分配对象

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

TimeKeeper *ptk = getTimeKeeper();     // 从TimeKeeper继承体系获得一个动态分配对象
...                                    // 运用它
delete ptk;                            // 释放它,避免资源泄露

上述代码的饿一个弱点是:纵使客户把每一件事都做对了,仍然没办法知道程序如何行动.

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

引来灾难的原因是,C++明确指出,当derived class 对象经由一个base class 指针来删除,而该base class 带着一个non-virtual 析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没有被销毁.如果getTimeKeeper返回指针指向一个AtomicClock对象,其内的AtomicClock成分很可能没被销毁,而AtomicClock的析构函数也未能执行起来.然而其base class 成分通常会被销毁,于是造成一个"局部销毁"对象,这可能形成资源泄露,败坏数据结构.

消除这个问题的办法很简单:给base class 一个 virtual 析构函数,此后删除derived class 对象就会达到目的.它会销毁整个对象,包括所有derived class 成分:

class TimeKeeper {
public:
    TimeKeeper();
    virtual ~TimeKeeper();
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk;                                // 现在行为正确

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

如果 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 函数实现细节不重要,重要的如果一个类内含 virtual 函数,其对象的体积会增加.C++的对象也不再和其他语言(如C)内的相同声明有着一样的结构,因此也就不能把它传递给其他语言所写的函数.

 因此,无端地将所有 class 的析构函数声明为 virtual,是错误的.通常的做法是:只有当 class 内含至少一个 virtual 函数,才为它声明 virtual 析构函数.

即使 class 完全不带 virtual 函数,被"non-virtual析构函数问题"给伤到还是有可能的.例如,标准string不含任何 virtual 函数,但有时程序员会错误地把它当做base class:

class SpecialString : public std::string {
};

如果在程序任意某处无意间将一个pointer-to-SpecialString转换为一个pointer-to-string,然后将转换所得到的那个string指针 delete 掉,则会出现之前的泄露问题.

相同的分析适用于任何不带 virtual 析构函数的 class,包括所有STL容器如vector,list,set等等.因此要拒绝继承一个标准容器或任何其他"带有non-virtual析构函数"的 class.

有时候令 class 带一个pure virtual 析构函数,可能颇为便利.pure virtual 函数导致抽象abstract class——即不能被实体化(instantiated)的 class.也就是说不能为那种类型创建对象.然而有时候希望拥有抽象 class,但没有任何pure virtual 函数,怎么办?由于抽象 class 总是被当作一个 base class 来用,而由于base class 应该有个 virtual 析构函数,并且由于pure virtual 函数会导致抽象 class,因此办法很简单:为希望它成为抽象的那个
class 声明一个 pure virtual 析构函数.例如:

class AWOV {
public:
    virtual ~AWOV() = 0;    // 声明pure virtual析构函数
};

这个 class 有一个pure virtual 函数,所以它是个抽象 class,又由于它有个 virtual 析构函数,所以无需担心析构函数的问题.这里有一个窍门:必须为这个pure virtual 析构函数提供一份定义:

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

析构函数的运作方式是:最深层派生(most derived)的那个 class 其析构函数最先被调用,然后是其每一个base class 的析构函数被调用.编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用操作,所以必须为这个函数提供一份定义.

定义纯虚析构函数的必要性:派生类的析构函数被调用,然后调用其每一个base class 的析构函数,最终抽象类的纯虚析构函数一定会被调用.如果纯虚析构函数只是声明,没有定义,那么就会造成运行时崩溃.而纯虚析构函数的空实现(如上面那行所示)能够保证这样的代码的安全性.

此外还可以利用纯虚成员函数定位错误代码(详见定义纯虚析构函数).

"给base classes一个virtual析构函数",这个规则只适用于polymorphic base classes上.这种base classes的设计目的是为了用来"通过base class接口处理derived class对象".TimeKeeper就是一个polymorphic base class,因为希望处理它的不同的派生类对象.

并非所有base classes的设计目的都是为了多态用途.例如标准string和STL容器都不被设计作为base classes使用,更不用提多态了.某些classes的设计目的是作为base classes使用,但不是为了多态用途,因此它们不需要virtual析构函数.

注意:

polymorphic base classes应该声明一个 virtual 析构函数.如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数.

classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不应该声明 virtual 析构函数.

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-25 22:27:13

Effective C++——条款7(第2章)的相关文章

Effective C++——条款13(第3章)

第3章    资源管理 Resource Management 所谓资源就是,一旦用了它,将来必须还给系统.C++程序中最常使用的资源就是动态内存分配(如果分配内存从来都增归还,会导致内存泄露).其他常见的资源还有文件描述符(file descriptors),互斥锁(mutex locks),图形界面中的字型和笔刷,数据库连接,以及网络sockets.不论哪一种资源,重要的是,不再使用它时,必须将它还给系统. 条款13:    以对象管理资源 Use objects to manage res

Effective C++——条款5(第2章)

第2章    构造/析构/赋值运算 Constructors,Destructors,and Assignment Operator 几乎每一个 class 都会有一个或多个构造函数,一个析构函数,一个copy assignment 操作符. 条款05:    了解C++默默编写并调用哪些函数 Know what functions C++ silently writes and calls 什么时候empty class 不再是个empty class 呢?当C++处理过它之后.如果自己没有声

Effective C++——条款15(第3章)

条款15:    在资源管理类中提供对原始资源的访问 Provide access to raw resources in resources-managing classes 资源管理类(resource-managing classes)很棒.它们是对抗资源泄露的堡垒.在一个良好的环境中将依赖这样的classes来处理和资源之间的所有互动.而不是直接处理原始资源,但这个环境并不完美,许多API直接涉及资源,因此有时只能绕过资源管理对象直接访问原始资源(raw resources). 例如,条

Effective C++——条款14(第3章)

条款14:    在资源管理类中小心copying行为 Think carefully about copying behavior in resource-managing classes 条款13导入这样的观念:"资源取得时机便是初始化时机"(Resource Acquisition Is Initializaiton,RAII),并以此作为"资源管理类"的脊柱,也描述了auto_ptr和tr1::shared_ptr如何将这个观念表现在heap-based资源

Effective C++——条款9(第2章)

条款09:    绝不在构造和析构过程中调用 virtual 函数 Never call virtual functions during construction or destruction 不应该在构造函数和析构函数期间调用 virtual 函数,因为这样的调用不会带来预想的结果. 假设有个 class 继承体系,用来模塑股市交易如买进,卖出的订单等等.这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录.下面是一个看起来颇为合理的做法: class Tr

Effective C++——条款6(第2章)

条款06:    若不想使用编译器自动生成的函数,就该明确拒绝 Explicitly disallow the use of compiler-generated functions you do not want. 在某些情况下,希望保持对象的唯一性,不想让对象有其他副本.如下: class HomeForSale { ... }; HomeForSale h1; HomeForSale h2; HomeForSale h3(h1); // 企图拷贝h1,不应该通过编译 h1 = h2; //

More Effective C++ 条款35 让自己习惯于标准C++ 语言

(由于本书出版于1996年,因此当时的新特性现在来说可能已经习以为常,但现在重新了解反而会起到了解C++变迁的作用) 1. 1990年后C++的重要改变 1). 增加了新的语言特性:RTTI,namespaces,bool,关键词mutable和explicit,enums作为重载函数之自变量所引发的类型晋升转换,以及"在class 定义区内直接为整数型(intergral) const static class members设定初值"的能力. 2). 扩充了Templates的特性

effective c++ 条款4 make sure that objects are initialized before they are used

1 c++ 类的数据成员的初始化发生在构造函数前 class InitialData { public: int data1; int data2; InitialData(int a, int b) { data1 = a: //this is assignment data2 = b; //this is assignment } /* InitialData(int a, int b):data1(a),data2(b) //this is initial {} */ } 2 不同cpp文

More Effective C++ 条款34 如何在一个程序中结合C++和C

1. C++和C混合使用的前提之一就是编译器产生兼容的目标文件(.lib和.dll等).所谓"兼容",指的是编译器在"预编译器相依的特性上"一致,如int和double大小,参数压栈机制等,只有在这个基础上才能讨论结合使用C++和C模块的问题. 2. 在1的基础上,要结合使用C++和C的模块,主要有以下几点需要注意: 1). name mangling(名称重整) Name mangling是C++用于支持函数重载的机制,它对重载的函数名称进行一定改变,使得每个函数