Effective C++ 学习笔记——条款07:为多态基类声明virtual析构函数

条款07:为多态基类声明virtual析构函数——在多态的基类中,把析构函数声明为虚函数

首先解释一下本节两个主要知识点:析构函数(destructor)与多态(polymorphism)

析构函数(destructor):用来释放对象所占用的资源。当对象的使用周期结束后,例如当某对象的范围(scope)结束时,或者是动态分配的对象被delete关键字解除资源时,对象的析构函数会被自动调用,对象所占用的资源就会被释放。假如在你的类中不声明析构函数,编译器也会为你自动生成一个。

多态(polymorphism):则是C++面向对象的基本思想之一,即抽象(abstraction),封装(encapsulation),继承(inheritance),多态(polymorphism)。如果我们希望仅仅通过基类指针就能操作它所有的子类对象,那这就是多态。

多态基类范例:

class TimeKeeper{                              // 计时器类,用来当做基类
    public:
    TimeKeeper();                              // 这是构造函数
    ~TimeKeeper();                             // 这是析构函数
    ......
};
class AtomicClock : public TimeKeeper{...};   // 原子钟是一种计时器
class WaterClock : public TimeKeeper{...};    // 水钟也是一种计时器

TimeKeeper* getTimeKeeper(){...}              // 用来返回一个动态分配的基类对象
TimeKeeper* ptk = getTimeKeeper();
.....                                         // 使用这个指针操作它的子类
delete ptk;                                   // 使用完毕,释放资源

以往情况下,我们通常认为 new 与 delete 相对应使用就不会发生内存泄漏(memory leak)。但是实际上多态中也可能发生内存泄漏,即使你及时使用了 delete。

上述代码存在的主要问题:
当你通过基类指针使用子类,使用完毕后却只从基类删除。同时这个基类的析构函数并不是虚函数(virtual),也就是不允许子类有自己版本的析构函数,这样就只能删除子类中基类的部分,而子类衍生出来的变量和函数所占用的资源并没有被释放,这就造成了这个对象只被释放了一部分资源的现象,依然会导致内存泄漏。

解决方法:
给基类一个虚的析构函数,这样子类就允许拥有自己的析构函数,就能保证被占用的所有资源都会被释放。

class TimeKeeper{
  public:
    virtual ~TimeKeeper();
  ....
};

注意:
其实作为一个多态的基类,不仅仅析构函数要声明为虚函数,如果想让不同的子类用不同的方法实现同一个函数,这个函数也要被声明为虚。换言之,大多数情况下,如果没有虚函数,这个类就不应该被用作一个基类。但也有少数例外会在后面提到。
另外若一个类不被用作基类,则不需要将其析构函数声明为虚函数,会产生不好的影响。

虚函数的工作原理:
虚函数是用来在运行时(runtime),自动把编译时未知的对象,比如用户输入的对象,和它所对应的函数绑定起来并调用。当一个类包含虚函数时,编译器会给这个类添加一个隐藏变量,即虚函数表指针(virtual table pointer),用来指向一个包含函数指针的数组,即虚函数表(virtual table)。当一个虚函数被调用时,具体调用哪个函数就可以从这个表里找了。

另外注意:
这个地址变量也是要占空间。例如在32位系统里,一个地址占32位,那么这个变量就要占32位,而在64位系统就要占用64位。

例如:

class Point{
  public:
    Point(...);
    ~Point();
  private:
    int x;
    int y;
};

注意:

  1. 这样一个Point的类包含两个整型,因此一个对象要占64位。但如果把析构函数声明为虚函数,在32位系统里就要多占32位,在64位系统里就要多占64位,那么它所占用的空间直接增大了50%到100%。这样一来,对象就刚好不能用一个64位的寄存器装下了。

  2. 另外,别的语言并没有C++这样的函数表指针,不知道怎么处理这个变量,所以就不能把这个对象从C++传到别的语言的程序里了。因此盲目声明虚函数也会给多语言项目带来不必要的麻烦。

错误案例:

class SpecialString : public std::string{...};      //某个继承自标准字符串的类

SpecialString* pss = new SpecialString("ID");
std::string* ps;
...
ps = pss;
delete ps;                                          //使用完后从基类删除内存。未有定义!现实中*ps的SpecialString资源会泄露,因为SpecialString析构函数没有被调用

这样的写法同样会导致一开始讲的内存泄漏,因为标准库的字符串并没有把析构函数定义为虚函数,它们并不是用来拿去继承的,所以不能随便继承,包括STL。虽然C++不像java有final和C#有sealed来阻止某些类被继承的机制,我们也要拒绝这种写法。

抽象类(abstract class):抽象类是包含至少一个纯虚函数的类(pure virtual function),而且它们不能被实例化,只能通过指针来操作,是纯粹被用来当做多态的基类的。

具体类(concrete class):虽然它们都可以通过父类指针来操作子类,但抽象类有更高一层的抽象,从设计的角度上能更好概括某些类的共同特性,比如"狗"相对于"边牧",“柴犬”,“斗牛”,把"狗"当做基类显然要好过把某个品种当做基类。

因为多态的基类需要有虚析构函数,抽象类又需要有纯虚函数,那么在抽象类中就要把析构函数声明为纯虚函数:

class AWSL{
  public:
    virtual ~AWSL() =0;              //"=0"只是一个关键字,用来声明纯虚函数,并不把任何东西设为0
};

同时注意:当在继承层级中某一类的析构函数被调用时,它下一级类的析构函数会被随后调用,最后一直到基类的析构函数,因此作为析构函数调用的终点,要保证有一个定义,否则链接器会报错。

AWSL::~AWSL(){}                     //基类的析构函数要有一个空的定义

一般来讲,我们使用基类都是为了实现多态,那么这些基类就需要虚的析构函数,比如我们的TimeKeeper类,就可以通过TimeKeeper的指针来操作例如AtomicClock这样的子类。

但并不是所有的基类都是被用来实现多态的,比如我们在上一章讲过的Uncopyable类,单纯只是为了实现某个功能,而不是希望通过它的指针来操作某个对象,那么就不需要将析构函数声明为虚函数。以及某些类就不是用来当做基类的,比如标准库的string类和STL容器类,也不需要将析构函数声明为虚函数。

总结:

  • 带有多态性质的基类应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,他就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性,就不应该声明 virtual 析构函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值