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.
顺序 选择 循环 总结