章节目录

override和final说明符

本节阅读量:

为了解决继承的一些常见挑战,C++有两个与继承相关的标识符:override和final。请注意,这些标识符不是关键字——它们是普通单词,只有在特定语境中使用时才具有特殊含义。C++标准称它们为“具有特殊含义的标识符”,但它们通常被称为“说明符”。

final 的使用场景不多,但 override 是一个非常实用的功能,你应该经常使用。在本课中,我们将研究这两种说明符,以及虚函数重写中“返回类型必须匹配”规则的一个例外。


override说明符

正如我们在上一课中提到的,只有当派生类虚函数的签名和返回类型与基类函数完全匹配时,它才被视为重写。这可能会导致无意中的问题,即原本期望作为重写的函数实际上不是。

考虑以下示例:

 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
#include <iostream>
#include <string_view>

class A
{
public:
	virtual std::string_view getName1(int x) { return "A"; }
	virtual std::string_view getName2(int x) { return "A"; }
};

class B : public A
{
public:
	virtual std::string_view getName1(short x) { return "B"; } // 注: 参数是 short
	virtual std::string_view getName2(int x) const { return "B"; } // 注: 函数是 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::getName1() 使用了不同的参数类型(short 而不是 int),因此它不被视为 A::getName1() 的重写。更隐蔽的是,B::getName2() 是 const 函数,而 A::getName2() 不是 const 函数,所以 B::getName2() 也不被视为 A::getName2() 的重写。

因此,该程序打印:

1
2
A
A

在这个特殊示例中,因为我们打印了 A 和 B,所以很容易看出重写出了问题,调用了错误的虚函数。然而,在更复杂的程序中,如果函数行为或返回值没有直接打印出来,这类问题可能很难调试。

为了帮助发现那些本应重写、但实际上没有重写的问题,可以将 override 说明符放在函数签名之后(与函数 const 说明符处于同一位置)。

如果函数没有重写基类函数(或应用于非虚函数),编译器将把该函数标记为错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <string_view>

class A
{
public:
	virtual std::string_view getName1(int x) { return "A"; }
	virtual std::string_view getName2(int x) { return "A"; }
	virtual std::string_view getName3(int x) { return "A"; }
};

class B : public A
{
public:
	std::string_view getName1(short int x) override { return "B"; } // 编译失败, 这个函数不是一个重写
	std::string_view getName2(int x) const override { return "B"; } // 编译失败, 这个函数不是一个重写
	std::string_view getName3(int x) override { return "B"; } // okay, 是 A::getName3(int) 的重写

};

int main()
{
	return 0;
}

上面的程序会产生两个编译错误:一个来自 B::getName1(),一个来自 B::getName2(),因为两者都没有重写 A 中的函数。B::getName3() 确实重写了 A::getName3(),因此这一行不会产生编译错误。

使用 override 说明符不会带来性能损失,而且它有助于确保您确实重写了自己认为应该重写的函数。因此,所有重写虚函数都应该使用 override 说明符进行标记。此外,由于 override 已经隐含 virtual 的含义,因此不需要同时使用 override 说明符和 virtual 关键字。


final说明符

在某些情况下,您可能不希望别人继续重写某个虚函数,或者继续从某个类继承。final 说明符可用于让编译器强制执行这一限制。如果用户试图重写 final 函数,或从指定为 final 的类继承,编译器将给出编译错误。

如果想限制用户重写函数,final 说明符的使用位置与 override 一致,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <string_view>

class A
{
public:
	virtual std::string_view getName() const { return "A"; }
};

class B : public A
{
public:
	// 注意下面使用了final说明符 -- 因此这个函数不能被子类重写
	std::string_view getName() const override final { return "B"; } // okay, 重写了 A::getName()
};

class C : public B
{
public:
	std::string_view getName() const override { return "C"; } // 编译失败: 重写 B::getName(), 但该函数是 final
};

在上面的代码中,B::getName() 重写了 A::getName()。但是 B::getName() 带有 final 说明符,这意味着任何进一步重写该函数的尝试都应被视为错误。事实上,C::getName() 试图重写 B::getName()(这里的 override 说明符是良好实践),所以编译器会给出编译错误。

如果我们想阻止从类继承,则在类名后应用final说明符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <string_view>

class A
{
public:
	virtual std::string_view getName() const { return "A"; }
};

class B final : public A // 注意final跟在类名后
{
public:
	std::string_view getName() const override { return "B"; }
};

class C : public B // 编译失败: 不能从final类继承
{
public:
	std::string_view getName() const override { return "C"; }
};

在上面的例子中,类 B 被声明为 final。因此,当 C 试图从 B 继承时,编译器将给出编译错误。


重写函数返回类型的一个例外

有一种特殊情况,派生类虚函数重写可以具有与基类不同的返回类型,但仍被视为匹配的重写。如果虚函数的返回类型是指针或对某个类的引用,则重写函数可以返回对派生类的指针或引用。这些被称为协变返回类型(covariant return types)。以下是一个示例:

 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
#include <iostream>
#include <string_view>

class Base
{
public:
	// getThis() 返回 Base 类的指针
	virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }
	void printType() { std::cout << "returned a Base\n"; }
};

class Derived : public Base
{
public:
	// 通常,重写函数的返回类型必须与基类函数的返回类型一致
	// 但是, 因为 Derived 从 Base 派生, 所以可以返回 Derived*
	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(); // 调用 Derived::getThis(), 返回 Derived*, 调用的是 Derived::printType
	b->getThis()->printType(); // 调用 Derived::getThis(), 返回 Base*, 调用的是 Base::printType

	return 0;
}

This prints:

1
2
3
4
called Derived::getThis()
returned a Derived
called Derived::getThis()
returned a Base

关于协变返回类型,有一个有趣的点需要注意:C++ 不能动态选择静态类型,所以你总是会得到与调用表达式的静态类型相匹配的返回类型。

在上面的例子中,我们首先调用 d.getThis()。由于 d 是 Derived,因此调用 Derived::getThis(),它返回 Derived*。然后,这个 Derived* 用于调用非虚函数 Derived::printType()。

接下来是一个更有趣的情况。我们调用 b->getThis()。变量 b 是一个指向派生对象的基类指针。Base::getThis() 是虚函数,因此实际调用的是 Derived::getThis()。虽然 Derived::getThis() 返回 Derived*,但由于该调用表达式的静态返回类型来自 Base 版本,也就是 Base*,因此返回的 Derived* 会被向上转换为 Base*。因为 Base::printType() 是非虚函数,所以最终调用的是 Base::printType()。

换句话说,在上面的例子中,只有当你先通过类型为 Derived 的对象调用 getThis() 时,才会得到 Derived*。

请注意,如果printType()是虚函数,那么b->getThis()(Base*类型的对象)的结果将经历虚函数解析,然后调用Derived::printType()。

协变返回类型通常用于虚成员函数返回指向本类的指针或引用的情况(例如,Base::getThis() 返回 Base*,Derived::getThis() 返回 Derived*)。然而,这并不是绝对必要的。


25.1 虚函数和多态

上一节

25.3 虚析构函数、虚赋值函数以及虚函数重写

下一节