final/override控制

本文介绍了C++11中的final与override关键字。final用于禁止派生类重写基类的虚函数,而override则确保派生类正确重写了基类的虚函数。这些特性有助于提高代码的可靠性和可维护性。

原文链接:http://book.2cto.com/201306/25355.html


在了解C++11中的final/override关键字之前,我们先回顾一下C++关于重载的概念。简单地说,一个类A中声明的虚函数fun在其派生类B中再次被定义,且B中的函数fun跟A中fun的原型一样(函数名、参数列表等一样),那么我们就称B重载(overload)了A的fun函数。对于任何B类型的变量,调用成员函数fun都是调用了B重载的版本。而如果同时有A的派生类C,却并没有重载A的fun函数,那么调用成员函数fun则会调用A中的版本。这在C++中就实现多态。

在通常情况下,一旦在基类A中的成员函数fun被声明为virtual的,那么对于其派生类B而言,fun总是能够被重载的(除非被重写了)。有的时候我们并不想fun在B类型派生类中被重载,那么,C++98没有方法对此进行限制。我们看看下面这个具体的例子,如代码清单2-23所示。

代码清单2-23
#include <iostream>
using namespace std;

class MathObject{
public:
    virtual double Arith() = 0;
    virtual void Print() = 0;
};

class Printable : public MathObject{
public:
   double Arith() = 0;
   void Print() // 在C++98中我们无法阻止该接口被重写
   {
       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, 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, y, z;
};
// 编译选项:g++ 2-10-1.cpp

在代码清单2-23中,我们的基础类MathObject定义了两个接口:Arith和Print。类Printable则继承于MathObject并实现了Print接口。接下来,Add2和Mul3为了使用MathObject的接口和Printable的Print的实现,于是都继承了Printable。这样的类派生结构,在面向对象的编程中非常典型。不过倘若这里的Printable和Add2是由两个程序员完成的,Printable的编写者不禁会有一些忧虑,如果Add2的编写者重载了Print函数,那么他所期望的统一风格的打印方式将不复存在。

对于Java这种所有类型派生于单一元类型(Object)的语言来说,这种问题早就出现了。因此Java语言使用了final关键字来阻止函数继续重写。final关键字的作用是使派生类不可覆盖它所修饰的虚函数。C++11也采用了类似的做法,如代码清单2-24所示的例子。

代码清单2-24
struct Object{
    virtual void fun() = 0;
};

struct Base : public Object {
    void fun() final;   // 声明为final
};

struct Derived : public Base {
    void fun();     // 无法通过编译
};
// 编译选项:g++ -c -std=c++11 2-10-2.cpp

在代码清单2-24中,派生于Object的Base类重载了Object的fun接口,并将本类中的fun函数声明为final的。那么派生于Base的Derived类对接口fun的重载则会导致编译时的错误。同理,在代码清单2-23中,Printable的编写者如果要阻止派生类重载Print函数,只需要在定义时使用final进行修饰就可以了。

读者可能注意到了,在代码清单2-23及代码清单2-24两个例子当中,final关键字都是用于描述一个派生类的。那么基类中的虚函数是否可以使用final关键字呢?答案是肯定的,不过这样将使用该虚函数无法被重载,也就失去了虚函数的意义。如果不想成员函数被重载,程序员可以直接将该成员函数定义为非虚的。而final通常只在继承关系的“中途”终止派生类的重载中有意义。从接口派生的角度而言,final可以在派生过程中任意地阻止一个接口的可重载性,这就给面向对象的程序员带来了更大的控制力。

在C++中重载还有一个特点,就是对于基类声明为virtual的函数,之后的重载版本都不需要再声明该重载函数为virtual。即使在派生类中声明了virtual,该关键字也是编译器可以忽略的。这带来了一些书写上的便利,却带来了一些阅读上的困难。比如代码清单2-23中的Printable的Print函数,程序员无法从Printable的定义中看出Print是一个虚函数还是非虚函数。另外一点就是,在C++中有的虚函数会“跨层”,没有在父类中声明的接口有可能是祖先的虚函数接口。比如在代码清单2-23中,如果Printable不声明Arith函数,其接口在Add2和Mul3中依然是可重载的,这同样是在父类中无法读到的信息。这样一来,如果类的继承结构比较长(不断地派生)或者比较复杂(比如偶尔多重继承),派生类的编写者会遇到信息分散、难以阅读的问题(虽然有时候编辑器会进行提示,不过编辑器不是总是那么有效)。而自己是否在重载一个接口,以及自己重载的接口的名字是否有拼写错误等,都非常不容易检查。

在C++11中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符override,如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。我们来看一下如代码清单2-25所示的这个简单的例子。

代码清单2-25
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;              // 无法通过编译,非虚函数重载
};
// 编译选项:g++ -c -std=c++11 2-10-3.cpp

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

函数名拼写错,Dijkstra误写作了Dikjstra。

函数原型不匹配,VNeumann函数的参数类型误做了double类型,而DKnuth的常量性在派生类中被取消了。

重写了非虚函数Print。

如果没有override修饰符,DerivedTop的作者可能在编译后都没有意识到自己犯了这么多错误。因为编译器对以上3种错误不会有任何的警示。这里override修饰符则可以保证编译器辅助地做一些检查。我们可以看到,在代码清单2-25中,DerivedTop作者的4处错误都无法通过编译。

此外,值得指出的是,在C++中,如果一个派生类的编写者自认为新写了一个接口,而实际上却重载了一个底层的接口(一些简单的名字如get、set、print就容易出现这样的状况),出现这种情况编译器还是爱莫能助的。不过这样无意中的重载一般不会带来太大的问题,因为派生类的变量如果调用了该接口,除了可能存在的一些虚函数开销外,仍然会执行派生类的版本。因此编译器也就没有必要提供检查“非重载”的状况。而检查“一定重载”的override关键字,对程序员的实际应用则会更有意义。

还有值得注意的是,如我们在第1章中提到的,final/override也可以定义为正常变量名,只有在其出现在函数后时才是能够控制继承/派生的关键字。通过这样的设计,很多含有final/override变量或者函数名的C++98代码就能够被C++编译器编译通过了。但出于安全考虑,建议读者在C++11代码中应该尽可能地避免这样的变量名称或将其定义在宏中,以防发生不必要的错误。http://book.2cto.com/201306/25355.html


<think>好的,用户现在问的是C++中overridefinal关键字的区别。之前他们询问过子类与父类同名函数时virtual的作用,现在可能是在进一步学习C++的面向对象特性,特别是关于继承和多态的部分。 首先,我需要明确overridefinal的用途。override用于显式指明子类覆盖父类的虚函数,而final则用于阻止进一步的覆盖或继承。这两个关键字都是在C++11中引入的,主要是为了提高代码的可读性和安全性。 接下来,我需要考虑用户可能的背景。他们可能已经了解虚函数和继承的基本概念,现在想深入理解如何更好地控制这些机制。用户可能在实际编程中遇到了问题,比如无意中隐藏了父类函数,或者希望某个类不能被继承,这时候overridefinal就派上用场了。 需要详细解释两者的区别,最好分点说明,并给出示例。比如,override确保函数确实覆盖了父类的虚函数,避免因为签名不匹配导致的错误。而final可以用于类或虚函数,防止进一步派生或覆盖。同时,要指出两者的使用场景,比如当设计一个基类时,某些函数不希望被进一步修改,可以用final;而override则用于明确覆盖关系,避免隐藏等问题。 还需要注意用户之前的提问中提到了virtual关键字,因此要确保这次回答和之前的连贯性,可能需要在比较中提到virtual的作用,但重点还是放在overridefinal的区别上。此外,用户可能对这两个关键字的语法和实际应用有疑问,所以示例代码会很有帮助,但需要符合之前的格式要求,比如使用正确的LaTeX格式和代码块。 最后,总结部分要简明扼要,强调两者的主要区别和适用情况,帮助用户快速理解和应用。同时,可能需要提醒一些常见错误,比如忘记使用override导致隐藏父类函数,或者错误地使用final限制了必要的扩展性。</think>在 C++ 中,`override` 和 `final` 是两个用于类继承和多态场景的关键字,**核心区别在于:`override` 用于明确「覆盖父类虚函数」,而 `final` 用于禁止「进一步覆盖或继承」**。以下是详细对比: --- ### 一、**`override` 关键字** #### **核心作用** - **显式声明覆盖**:明确表示子类函数覆盖父类虚函数,**强制编译器检查函数签名是否匹配**,避免因函数名、参数或返回类型不匹配导致的隐藏(隐藏会导致多态失效)。 - **增强代码可读性**:直接表明该函数是父类虚函数的覆盖版本。 #### **使用场景** 子类覆盖父类虚函数时,在函数声明后添加 `override`。 #### **示例** ```cpp class Parent { public: virtual void foo() { /* ... */ } virtual int bar(int x) { /* ... */ } }; class Child : public Parent { public: void foo() override { // ✅ 正确覆盖 Parent::foo /* ... */ } // ❌ 编译错误:参数不匹配,未正确覆盖 Parent::bar(int) int bar(double x) override { /* ... */ } }; ``` #### **关键点** - **强制签名检查**:若父类没有同名虚函数,或签名不匹配,编译器会报错。 - **仅用于虚函数**:只能修饰覆盖父类虚函数的子类函数。 --- ### 二、**`final` 关键字** #### **核心作用** - **禁止覆盖**:修饰虚函数时,阻止子类进一步覆盖该函数。 - **禁止继承**:修饰类时,阻止其他类继承该类。 #### **使用场景** 1. 设计不可修改的虚函数(如关键算法、接口固定行为)。 2. 设计不可继承的类(如工具类、单例类)。 #### **示例** **场景 1:禁止覆盖虚函数** ```cpp class Parent { public: virtual void foo() final { // 标记为 final,禁止子类覆盖 /* ... */ } }; class Child : public Parent { public: // ❌ 编译错误:不能覆盖 final 函数 void foo() override { /* ... */ } }; ``` **场景 2:禁止继承类** ```cpp class Utility final { // 标记为 final,禁止继承 public: static void helper() { /* ... */ } }; // ❌ 编译错误:无法继承 final 类 class SubUtility : public Utility { /* ... */ }; ``` #### **关键点** - **可修饰函数或类**:`final` 可修饰虚函数或类,`override` 仅修饰函数。 - **终止继承链**:用于终止多态行为或类继承关系。 --- ### 三、**对比总结** | **特性** | `override` | `final` | |--------------------|-------------------------------------|--------------------------------------| | **目的** | 明确覆盖父类虚函数,强制签名检查 | 禁止进一步覆盖或继承 | | **修饰对象** | 函数 | 函数或类 | | **使用场景** | 子类覆盖父类虚函数时 | 设计不可修改的虚函数或不可继承的类 | | **编译检查** | 检查是否匹配父类虚函数签名 | 检查是否尝试覆盖 final 函数/继承类 | | **多态影响** | 支持多态 | 终止多态链或继承链 | --- ### 四、**联合使用示例** ```cpp class Parent { public: virtual void foo() { /* ... */ } }; class Child : public Parent { public: void foo() override final { // 覆盖父类虚函数,并禁止子类进一步覆盖 /* ... */ } }; class GrandChild : public Child { public: // ❌ 编译错误:Child::foo 是 final,不可覆盖 void foo() override { /* ... */ } }; ``` --- ### 五、**实际开发建议** 1. **优先使用 `override`**: - 所有覆盖父类虚函数的子类函数都应添加 `override`,避免隐藏问题(如参数错误、父类未声明虚函数等)。 - 示例:`void foo() override { ... }` 2. **谨慎使用 `final`**: - 仅在明确禁止扩展时使用(如安全关键代码、性能优化场景)。 - 示例:`virtual void criticalFunc() final { ... }` 3. **结合使用场景**: ```cpp class ImmutableBase final { // 禁止继承 public: virtual void api() const = 0; }; class Utility { public: virtual void helper() final { // 禁止覆盖 /* ... */ } }; ``` --- 通过合理使用 `override` 和 `final`,可以更安全地设计类层次结构,明确代码意图,并减少因继承和多态引发的潜在错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值