重写(override)、重载(overload)和隐藏(overwrite)在C++中是3个完全不同的概念,但是在平时的工作交流中,我发现有很多C++程序员对它们的概念模糊不清,经常误用或者混用这3个概念,所以在说明override说明符之前,我们先梳理一下三者的区别。
在编码过程中,重写虚函数很容易出现错误,原因是C++语法对重写的要求很高,稍不注意就会无法重写基类虚函数。更糟糕的是,即使我们写错了代码,编译器也可能不会提示任何错误信息,直到程序编译成功后,运行测试才会发现其中的逻辑问题,例如:
class Base {
public:
virtual void some_func() {}
virtual void foo(int x) {}
virtual void bar() const {}
void baz() {}
};
class Derived : public Base {
public:
virtual void sone_func() {}
virtual void foo(int &x) {}
virtual void bar() {}
virtual void baz() {}
};
以上代码可以编译成功,但是派生类Derived的4个函数都没有触发重写操作。第一个派生类虚函数sone_func的函数名与基类虚函数some_func不同,所以它不是重写。第二个派生类虚函数foo(int &x)的形参列表与基类虚函数foo(int x)不同,所以同样不是重写。第三个派生类虚函数bar()相对于基类虚函数少了常量属性,所以不是重写。最后的基类成员函数baz根本不是虚函数,所以派生类的baz函数也不是重写。
可以看到重写如此容易出错,光靠人力排查避免出错是很困难的,尤其当类的继承关系非常复杂的时候。所以C++11标准提供了一个非常实用的override说明符,这个说明符必须放到虚函数的尾部,它明确告诉编译器这个虚函数需要覆盖基类的虚函数,一旦编译器发现该虚函数不符合重写规则,就会给出错误提示。
class Base {
public:
virtual void some_func() {}
virtual void foo(int x) {}
virtual void bar() const {}
void baz() {}
};
class Derived : public Base {
public:
virtual void sone_func() override {}
virtual void foo(int &x) override {}
virtual void bar() override {}
virtual void baz() override {}
};
在C++中,我们可以为基类声明纯虚函数来迫使派生类继承并且重写这个纯虚函数。但是一直以来,C++标准并没有提供一种方法来阻止派生类去继承基类的虚函数。C++11标准引入final说明符解决了上述问题,它告诉编译器该虚函数不能被派生类重写。final说明符用法和override说明符相同,需要声明在虚函数的尾部。
class Base {
public:
virtual void foo(int x) {}
};
class Derived : public Base {
public:
void foo(int x) final {};
};
class Derived2 : public Derived {
public:
void foo(int x) {};
};
在上面的代码中,因为基类Derived的虚函数foo声明为final,所以派生类Derived2重写foo函数的时候编译器会给出错误提示。
请注意final和override说明符的一点区别,final说明符可以修饰最底层基类的虚函数而override则不行,所以在这个例子中final可以声明基类Base的虚函数foo,只不过我们通常不会这样做。
有时候,override和final会同时出现。这种情况通常是由中间派生类继承基类后,希望后续其他派生类不能修改本类虚函数的行为而产生的,举个例子:
class Base {
public:
virtual void log(const char *) const {…}
virtual void foo(int x) {}
};
class BaseWithFileLog : public Base {
public:
virtual void log(const char *) const override final {…}
};
class Derived : public BaseWithFileLog {
public:
void foo(int x) {};
};
在上面这段代码中基类Base有一个虚函数log,它将日志打印到标准输出。但是为了能更好地保存日志,我们写了一个派生类BaseWithFileLog,重写了log函数将日志写入文件中。为了保证重写不会出现错误,并且后来的继承者不要改变日志的行为,为log函数添加了override和final说明符。这样一来,后续的派生类Derived只能重写虚函数foo而无法修改日志函数,保证了日志的一致。
最后要说明的是,final说明符不仅能声明虚函数,还可以声明类。如果在类定义的时候声明了final,那么这个类将不能作为基类被其他类继承,例如:
class Base final {
public:
virtual void foo(int x) {}
};
class Derived : public Base {
public:
void foo(int x) {};
};
在上面的代码中,由于Base被声明为final,因此Derived继承Base会在编译时出错。
为了和过去的C++代码保持兼容,增加保留的关键字需要十分谨慎。因为一旦增加了某个关键字,过去的代码就可能面临大量的修改。所以在C++11标准中,override和final并没有被作为保留的关键字,其中override只有在虚函数尾部才有意义,而final只有在虚函数尾部以及类声明的时候才有意义,因此以下代码仍然可以编译通过:
class X {
public:
void override() {}
void final() {}
};
不过,为了避免不必要的麻烦,建议读者不要将它们作为标识符来使用。