• final/override 控制


    1.重载(overloading)与重写(overriding)

    在学习 final/override之前,先学习一下 重载(overloading)重写(overriding) 的区别

    区别点重载(overloading)重写(overriding)
    定义方法名称相同
    参数类型或个数不同
    virtual关键字可有可无
    方法名称,参数类型,
    返回值类型全部相同
    基类函数必须有virtual关键字
    范围发生在一个类中发生在继承类中
    #include
    using namespace std;
    
    class A//父类{
    public:
        A() {}
        virtual void foo(){
            cout << "This is A." << endl;
        }
        void foo(int x)//实现了函数的重载{
            cout << "This is A." << x <<endl;
        }
    };
    
    class B : public A//子类{
    public:
        B() {}
        void foo()//实现了函数的重写{
            cout << "This is B." << endl;
        }
    };
    
    int main()
    {
        A a;
        B b;
        a.foo();
        a.foo(1);
        b.foo();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    1.2 final

      然后回到正题,在通常情况下,一旦在基类A中的成员函数fun被声明为virtual的,那么对于其派生类B而言,fun总是能够被重载的(除非被重写了),有的时候我们并不想fun在B类型派生类中被重载,c++98没有方法对此进行限制。如下代码所示

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

      在上述代码中,基类MathObject定义了两个接口Arith和Print。Printable则是继承了基类,并只实现了Print,然后Add2和Mul3又继承了Printable,但是在Add2和Mul3中也是可以重写Print的,但是如果Printable和Add2是两个不同的人员编写,这个时候如果Add2的编写者重写了Print吗,那么Printable编写者期望的打印风格将会改变,所以这个时候final就能派上用场,在函数后面加上该关键字就可以禁止该函数的重写。final关键字的作用是使派生类不可覆盖它所修饰的虚函数

    struct Object
    {
    	virtual void fun() = 0;
    };
    
    struct Base : public Object
    {
    	void fun() final; // 声明为 final
    };
    
    struct Derived : public Base
    {
    	void fun(); // 无法通过编译
    	//void fun(int x); // 重定义是允许的
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

      在上述代码中,final也是可以放在基类Object里修饰fun,但是如果这样的话就失去了虚函数的意义,虚函数本来就是用来继承给子类的,所以一般不会这样做。

    1.3 override

      在c++11中,override被称作函数重写,但它还有一个含义是虚函数描述符,它是一个关键字,而函数重写的override只是一个名称。
      在c++重载中有一个特点,就是对于基类中的声明为virtual的函数,之后的重载版本不再需要申明改重载函数为virtual,也就是说不在需要写virtual关键字。即使在派生类中声明了virtual,该关键字也是编译器可以忽略的。虽然说这样可以带来一些书写上的便利,却带来了阅读上的困难,就如Printable中的Print函数,程序员无法从Printable看出来Print是虚函数还是一个非虚函数。另外一点就是在c++中有的虚函数会“跨层”,也就是说没有在父类中声明的接口有可能是祖先的虚函数接口,如下代码所示,在Printable中没有定义Arith,但是在Add2和Mul3依旧是可以重写的,这同样是在父类中无法读到相应的信息。

    #include 
    
    using namespace std;
    
    class MathObject{
    public:
    	virtual double Arith() = 0;
    	virtual void Print() = 0;
    };
    
    class Printable : public MathObject{
    public:
        //没有定义Arith
    	void Print() {
    		cout << "Output is: " << Arith() << endl;
    	}
    };
    
    class Add2 : public Printable
    {
    public:
    	Add2(double a, double b) : x(a), y(b) {}
    	double Arith() { return x + y; }//跨层
    private:
    	double x;
    	double y;
    };
    
    class Mul3 : public Printable
    {
    public:
    	Mul3(double a, double b, double c) : x(a), y(b), z(c) {}
    	double Arith() { return x * y * z; }//跨层
    private:
    	double x;
    	double y;
    	double z;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

      这样一来,如果类的继承结构比较长(不断地派生)或者比较复杂(比如偶尔多重继承),派生类的编写者会遇到信息分散、难以阅读的问题(虽然有时候编辑器会进行提示,不过编辑器不是总是那么有效)。而自己是否在重载一个接口,以及自己重载的接口的名字是否有拼写错误等,都非常不容易检查
    ++在C++11中为了帮助程序员写继承结构复杂的类型,引人了虚函数描述符override,如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数!否则代码将无法通过编译。++

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

      在上述代码中,我们在基类Base中定义了一些virtual的函数(接口)以及一个非Virtual的函数Print。其派生类DerivedMid中,基类的Base的接口都没有重载,不过通过注释可以发现,DerivedMid的作者曾经想要重载出一个“voidVNeumann(doubleg)”的版本。这行注释显然迷惑了编写DerivedTop的程序员,所以DerivedTop的作者在重载所有Base类的接口的时候,犯下了3种不同的错误:

    • 函数名拼写错,Dijkstra误写作了Dikjstraa
    • 函数原型不匹配,VNeumann函数的参数类型误做了double类型,而DKnuth的常量性在派生类中被取消了。
    • 重写了非虚函数Print

      如果没有override修饰符,DerivedTop的作者可能在编译后都没有意识到自己犯了这么多错误。因为编译器对以上3种错误不会有任何的警示。这里override修饰符则可以保证编译器辅助地做一些检查。

      此外,值得指出的是,在C++中,如果一个派生类的编写者自认为新写了一个接口,实际上却重载了一个底层的接口(一些简单的名字如get、set、print就容易出现这样的状况),出现这种情况编译器还是爱莫能助的。不过这样无意中的重载一般不会带来太大的问题,因为派生类的变量如果调用了该接口,除了可能存在的一些虚函数开销外,仍然会执行派生类的版本。因此编译器也就没有必要提供检查“非重载”的状况。而检查“一定重载”的override关键字,对程序员的实际应用则会更有意义。
      还有值得注意的是final/override也可以定义为正常变量名,只有在其出现在函数后时才是能够控制继承/派生的关键字。通过这样的设计,很多含有final/override变量或者函数名的C++98代码就能够被C++编译器编译通过了。但出于安全考虑,建议读者在C++11代码中应该尽可能地避免这样的变量名称或将其定义在宏中,以防发生不必要的错误。

    总结

    重载和重写的定义(详见开头的表格)
    final关键字的作用是使派生类不可覆盖它所修饰的虚函数,也就是说final修饰的函数不能重写(overriding)
    如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数!否则代码将无法通过编译,也就是说如果父类中使用override修饰了某个虚函数,那么子类必须重载该函数

  • 相关阅读:
    BasePopup - Android下打造通用便捷的PopupWindow弹窗库
    燃烧化学平衡判据
    记一次springboot项目结合arthas排查ClassNotFoundException问题
    cornerstone.js 中PT图像自己计算窗宽窗位
    飞步科技与梅东公司合作第四年:港口L4无人集卡车队突破60台
    ZStack Cloud 4.4.24 新功能:内存快照技术详解
    Spring注解驱动之FactoryBean注册组件
    app逆向(10)| APP的加固与脱壳
    Windows使用小技巧
    Java语句
  • 原文地址:https://blog.csdn.net/yaoyaohyl/article/details/127423752