C++11 final/override控制

严正声明:如果没有搞明白重载、覆盖、隐藏等基础概念,请先参见随笔《重载 覆盖 隐藏

【1】为什么要引入final关键字?

在通常情况下,一旦在基类A中的成员函数fun被声明为virtual的,那么对于其派生类B而言,fun总是能够被重写的。

有的时候我们并不想fun在派生类B中被重写,那么,C++98没有方法对此进行限制。如下示例:

 1 class MathObject
 2 { 
 3 public:
 4     virtual double Arith() = 0;
 5     virtual void Print() = 0;
 6 };
 7 
 8 class Printable : public MathObject
 9 { 
10 public:
11     double Arith() = 0;
12     void Print()   // 在C++98中我们无法阻止该接口在其派生类被重写
13     { 
14         cout << "Output is: " << Arith() << endl;
15     }
16 };
17 
18 class Add2 : public Printable
19 { 
20 public: 
21     Add2(double a, double b) : x(a), y(b) 
22     {} 
23 
24     double Arith()
25     {
26         return x + y;
27     } 
28 private:
29     double x, y;
30 }; 
31 
32 class Mul3 : public Printable
33 {
34 public:
35     Mul3(double a, double b, double c) : x(a), y(b), z(c)
36     {}
37 
38     double Arith()
39     { 
40         return x * y * z;
41     } 
42 private: 
43     double x, y, z;
44 };

基础类MathObject定义了两个接口:Arith和Print。

类Printable则继承于MathObject并实现了Print接口。

接下来,Add2和Mul3为了使用MathObject的接口和Printable类中Print的实现,于是都继承了Printable。

这样的类派生结构,在面向对象的编程中非常典型。

不过倘若这里的Printable和Add2是由两个程序员完成的,Printable的编写者不禁会有一些忧虑,如果Add2的编写者重载了Print函数,那么他所期望的统一风格的打印方式将不复存在。

对于Java这种所有类型派生于单一元类型(Object)的语言来说,这种问题早就出现了。

因此Java语言使用了final关键字来阻止函数继续重写。

final关键字的作用是使派生类不允许再重写它所修饰的虚函数。

C++11也采用了类似的做法,如下示例:

 1 struct Object
 2 { 
 3     virtual void fun() = 0;
 4 };
 5 
 6 struct Base : public Object
 7 {
 8     void fun() final; // 声明为final
 9 };
10 
11 struct Derived : public Base
12 { 
13     void fun();      // 无法通过编译
14 };

 取Java的长处补自己的短处,这个机制得点个大赞。

注意以上内容的言外之意:不可使用final修饰非虚函数。

【2】为什么要引入关键字override?

C++中的重写还有一个特点,就是对于基类声明为virtual的函数,之后的派生类版本都不需要再声明该函数为virtual。

即使在派生类中声明了virtual,该关键字也是编译器可以忽略的。

这带来了一些书写上的便利,却带来了一些阅读上的困难。

比如上例中的Printable的Print函数,程序员无法从Printable的定义中看出Print是一个虚函数还是非虚函数。

另外一点就是,在C++中有的虚函数会“跨层”,没有在父类中声明的接口有可能是祖先的虚函数接口。

比如上例中,如果Printable不声明Arith函数,其接口在Add2和Mul3中依然是可重载的,这同样是在父类中无法读到的信息。

这样一来,如果类的继承结构比较长(不断地派生)或者比较复杂(比如偶尔多重继承),派生类的编写者会遇到信息分散、难以阅读的问题(虽然有时候编辑器会进行提示,不过编辑器不是总是那么有效)。

而自己是否在重载一个接口,以及自己重载的接口的名字是否有拼写错误等,都非常不容易检查。

在C++11中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符override。

如果派生类在虚函数声明时使用了override描述符,那么该函数必须重写其基类中的同名函数,否则代码将无法通过编译。

如下示例:

 1 struct Base
 2 { 
 3     virtual void Turing() = 0;
 4     virtual void Dijkstra() = 0;
 5     virtual void VNeumann(int g) = 0;
 6     virtual void DKnuth() const;
 7     void Print();
 8 };
 9 
10 struct DerivedMid : public Base
11 {
12 //  void VNeumann(double g);  // 接口被隔离了,曾想多一个版本的VNeumann函数
13 };
14 
15 struct DerivedTop : public DerivedMid
16 { 
17     void Turing() override;
18     void Dikjstra() override;          // 无法通过编译,拼写错误,并非要重写
19     void VNeumann( double g) override; // 无法通过编译,参数不一致,并非要重写
20     void DKnuth() override; // 无法通过编译,常量性不一致,并非要重写
21     void Print() override;  // 无法通过编译,非虚函数不可使用override修饰
22 };

注释可以完美解释一切,不再赘述。

个人认为,override关键字主要是为了弥补virtual关键字的不足。

 

good good study, day day up.

顺序 选择 循环 总结

posted @ 2020-01-23 16:00  kaizenly  阅读(527)  评论(0编辑  收藏  举报
打赏