• virtual
  • override
  • final

上面三个关键字在继承体系中起着重要作用。virtual不用多说,声明虚函数必备的关键字。overridefinal是 C++11 中引入的,二者的作用其实更多是提醒开发者自己,

  • 我正在重写一个虚函数
  • 我正在重写一个虚函数,并且不希望再被派生类重写这个函数

当然,final也可以用来修饰类,表示这是最后一次继承,即我这个类不能再被继承了。很形象,例如

1
2
3
4
class A {};
class B : public A {};
class C final : pubilc B {};
class D : public C {}; // compile error!

但除此之外,finaloverride对编译器而言,确实有助益。例如,

  • 如果你用overridefinal修饰一个函数,但基类没有这个函数,或者这个函数在基类没有被声明为虚函数,就会引发编译错误。当然,你可能只是手滑打错字了。但确实,编译错误帮你很快定位到这个问题。
  • 如果你用overridefinal修饰一个函数,你就不必再用virtual修饰。因为这两个修饰已经暗示这个函数是虚函数。当然,如果基类中声明已经声明了virtual,无论你用不用这些修饰词,这个函数在派生类中都是虚函数。

然而,有一个很纠结的问题。在多态体系中,基类的析构函数必须是[[../notes/cpp/多态#When should my destructor be virtual?|虚]]的。这就要问了,那对于派生类的析构函数,到底要不要声明为virtual,又或者用overridefinal修饰呢?这个问题很让人纠结,从下面这个 issue 出发,你可以看到大家的讨论

override修饰析构函数

不过我自己的建议是,对于派生类的析构函数,总是用override修饰。如果你这么做了,你将得到如下收益:

  1. 编译器确保基类的析构函数是虚的,否则编译报错。真的有人会忘记将基类析构函数声明为虚的。
  2. 你可以直观看出,当前这个类有一个虚析构函数。

同时,这也会因此困惑,

  1. 析构函数可以被重写吗?你不敢确信,于是上网搜寻相关资料,发现并不能重写析构函数。过了一个月,你看到这段代码,又重演一遍上述剧情!

确实,override修饰析构函数会造成困惑!但是,它利大于弊。事实上,虚析构函数中的“虚”和普通的虚函数有着不一样的语义。对于普通成员函数,让他成为虚函数的目的是,我摆明了想在派生类中重写它,进而达到多态的效果。而析构函数的虚,是一种机制上的必须。当你用基类指针或引用使用[[../notes/cpp/多态#Dynamic binding|动态绑定]]时,对象的销毁依赖于虚析构函数。如果基类的析构函数非虚,那么对象销毁时,只会调用基类的析构函数,这可能造成派生类的资源没有释放,进而导致内存泄漏。而如果基类的析构函数是虚的,那么会调用到派生类的析构函数,而派生类的析构函数保证会调用基类的析构函数(C++标准保证),这样一来,就能保证资源以合理的顺序释放。

所以,用override修饰析构函数,并不是重写基类的析构函数(事实上我们也无法做到),而是在提醒编译器检查基类的虚构函数必须是虚的。

为什么不用final修饰析构函数?

事实上,final用来修饰函数,可以防止这个函数继续被派生类重写。但是,每个类都必须有析构函数!一旦析构函数被final修饰,那么这个类将无法再被继承。因为继承这个类,默认会带上生成析构函数,无论用不用这三个关键字修饰。然而,这个行为被基类的final阻止了,冲突了。这里,编译器认为是重写了被final修饰的函数,编译无法通过。

为什么不用virtual修饰析构函数?

可以,但没必要。因为如果基类析构函数已经是虚的,那么派生类析构函数自然而然也是虚的。用virtual修饰不会增加任何编译检查,因为virtual是声明虚函数的,对编译器没有任何提示作用。即便是基类没有的函数,派生类用virtual修饰的函数也会成为虚函数被进一步派生重写。