Skip to content

18.3 重写 final 标识符,并且协变返回类型

By Alex on November 6th, 2016 | last modified by nascardriver on December 12th, 2020

翻译 by 赵文杰 2020-12-20 | 最后修改于 2020-12-20

为了解决一些使用继承过程常见的问题,C++ 添加了两种特殊的标识符:ovveridefinal。注意这些标识符不是关键词 —— 他们是有特殊意义的普通的标识符。

尽管 final 不是很常用,重写是一个你应该规律使用的神奇的功能。在这节课中,我们也会看一眼这两个问题,以及虚拟函数重写返回类型必须匹配的规则的一个例外。

ovveride 说明符

我们在之前的课程中提到过,一个派生类虚函数如果恰好匹配返回值类型就是重写。这会无意中引起问题,一个打算重写的函数实际上并没有重写。

思考下列例子:

class A
{
public:
 virtual const char* getName1(int x) { return "A"; }
 virtual const char* getName2(int x) { return "A"; }
};

class B : public A
{
public:
 virtual const char* getName1(short int x) { return "B"; } // note: parameter is a short int
 virtual const char* getName2(int x) const { return "B"; } // note: function is const
};

int main()
{
 B b{};
 A& rBase{ b };
 std::cout << rBase.getName1(1) << '\n';
 std::cout << rBase.getName2(2) << '\n';

 return 0;
}

因为 rBase 是一个指向 B 对象的 A 类引用,目的是使用虚函数来访问 B::getName1()B::getName2()。然而,因为 B::getName() 携带了一个不同的参数(short int 而不是 int),它不被当做是一个 A::getName1() 的重写。在不知不觉中, B::getName2 是一个 const 函数,而 A::getName2() 不是,B::getName2() 也不会成为 A::getName2() 的重写。

因此,程序会打印:

A
A

在这个例子中,因为 A 和 B 只打印了他们的名字,很容易能看出我们搞乱了这些重写函数,然后错误的虚函数就被调用了。然而,在更加复杂的程序当中,函数有什么行为或者返回值没有被打印,这样的问题可能很难调试、

为了帮助解决本来要重写但是实际上并没有的问题,C++11 引入了重写说明符。override 说明符可以被应用到任何重写函数,只要摆在 const 所在的位置就可以。如果这个函数实际上没有重写一个基类的函数(或者被使用到了非虚函数,编译器就会标注这个函数为一个错误)。

class A
{
public:
 virtual const char*getName1(int x) { return "A"; }
 virtual const char* getName2(int x) { return "A"; }
 virtual const char* getName3(int x) { return "A"; }
};

class B : public A
{
public:
 virtual const char*getName1(short int x) override { return "B"; } // compile error, function is not an override
 virtual const char* getName2(int x) const override { return "B"; } // compile error, function is not an override
 virtual const char* getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)

};

int main()
{
 return 0;
}

上方的程序产生两个编译错误:一个是 B::getName1() 然后一个是 B::getName(),因为他们都没有重写了之前的函数。B::getName3() 确实重写了 A::getName(),因此没有错误产生。

使用重写说明符没有任何性能损失,帮助我们避免无意中犯错。因此,我们非常建议在每个虚函数的重写中使用 override 来确保你重写了你能像你如期重写函数。

规则:

给你写的每一个想要重载的函数添加 override 说明符

final 说明符

有时候你可能不希望你的函数重写了虚函数,或者从一个类继承。final 说明符可以被用来告诉编译器强调这个。如果用户尝试重写一个基类中的函数,并且这个虚函数链已经被标记为 final,编译器会报错。

在这个例子中你想要限制用户重载函数,final 说明符可以替换 ovrride 说明符,像这样:

class A
{
public:
 virtual const char* getName() { return "A"; }
};

class B : public A
{
public:
 // note use of final specifier on following line -- that makes this function no longer overridable
 virtual const char* getName() override final { return "B"; } // okay, overrides A::getName()
};

class C : public B
{
public:
 virtual const char* getName() override { return "C"; } // compile error: overrides B::getName(), which is final
};

在上面的代码中,B::getName() 重写了 A::getName(),没什么错误。但是 B:;getName() 有一个 final 说明符,意思是任何尝试重写这个函数都会被认为是一个错误。事实上,C::getName() 尝试重写 B::getName()override 关键词并不是必要的,只是为了良好的习惯),因此编译器报一个编译错误。

在这个例子中我们想要阻止任何类的继承,类名后可以添加一个 final 说明符来实现:

class A
{
public:
 virtual const char* getName() { return "A"; }
};

class B final : public A // note use of final specifier here
{
public:
 virtual const char* getName() override { return "B"; }
};

class C : public B // compile error: cannot inherit from final class
{
public:
 virtual const char* getName() override { return "C"; }
};

在上方的例子中,B 类以 final 关键词申明,当 C 尝试从 B 继承的时候,编译器就会报出一个编译错误。

Covariant 返回类型

有一种特殊的情况,当一个派生类虚函数重写可以和基类有不同的返回类型,并且仍然能匹配重载。如果一个虚函数的返回值类型是一个类的指针或者引用,重写函数可以返回一个指针或者引用到一个派生类。这叫做 covariant return types。例如:

# include <iostream>

class Base
{
public:
 // This version of getThis() returns a pointer to a Base class
 virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }
 void printType() { std::cout << "returned a Base\n"; }
};

class Derived : public Base
{
public:
 // Normally override functions have to return objects of the same type as the base function
 // However, because Derived is derived from Base, it's okay to return Derived*instead of Base*
 Derived* getThis() override { std::cout << "called Derived::getThis()\n";  return this; }
 void printType() { std::cout << "returned a Derived\n"; }
};

int main()
{
 Derived d{};
 Base*b{ &d };
 d.getThis()->printType(); // calls Derived::getThis(), returns a Derived*, calls Derived::printType
 b->getThis()->printType(); // calls Derived::getThis(), returns a Base*, calls Base::printType

 return 0;
}

打印:

called Derived::getThis()
returned a Derived
called Derived::getThis()
returned a Base

注意,一些老旧的编译器(如 Visual Studio 6 )不支持这种可变返回类型。

一个有意思的关于 covariant return types 的事情就是: C++ 不能动态的选择而类型,因此您将始终获得与被调用函数的基类版本相匹配的类型。(so you’ll always get the type that matches the base version of the function being called.)

在上方的例子中,我们先调用了 d.getThis()。因为 d 是一个派生类,这调用了 Derived::getThis(),返回了一个 Derived*。这 Derived* 紧接着调用了一个非虚函数 Derived::printType()

现在来看一个有趣的例子。我们接着调用 d->getThis()。变量 b 是一个基类指针指向 Derived 类的对象。Base::getThis() 是一个虚函数,因此这调用了 Derived::getThis()。尽管 Derived::getThis() 返回了一个 Derived*,因为基类版本的函数返回一个 Base*,返回的 Derived* 会向上转化为一个 Base*。所以,Base::printType() 被调用了。

(未翻译)In other words, in the above example, you only get a Derived* if you call getThis() with an object that is typed as a Derived object in the first place.