关键字 override
没有 override 时存在的风险
面试官: 在 C++11 之后,我们在处理类的继承和虚函数时,多了 override 和 final 这两个说明符。我们先来聊聊 override。
请看下面这段代码:
| |
在 Derived 类中,程序员的意图是重写 (override) 基类的 doSomething 函数。但由于笔误,他把参数类型从 int 写成了 long。
请问,在没有 override 关键字的情况下,这段代码能编译通过吗?如果能,它在多态调用时,可能会产生什么样的问题?
回答:
- 我认为编译能够通过。由于参数列表不同(这里是参数类型不同),编译器会认为程序员在
Derived类中重载了一个虚函数。 - 可能的问题:在多态调用时,会出现函数参数实际类型不一致的情况,可能导致冲突。
面试官 (补充与解释): 你说的很对。我们来把这个问题分析得更透彻一些。
为什么能编译通过?
因为编译器看到 Derived::doSomething(long) 时,它并不认为你是在“重写”。由于参数列表不同 (long vs int),编译器会认为你是在 Derived 类里定义了一个全新的、和基类函数重载 (overload) 的虚函数。
会产生什么问题? 这会导致一个非常隐蔽且危险的 bug,我们称之为 “意外重载 (accidental overload)” 而不是 “有意重写 (intentional override)”。
看下面的多态调用:
| |
- 编译器看到
p->doSomething(10)。 p的静态类型是Base*,10的类型是int。- 编译器在
Base类里找到了一个完美匹配的virtual void doSomething(int)。 - 因为它是
virtual的,所以程序会在运行时进行动态绑定。 - 在运行时,系统检查
p实际指向的Derived对象,去看它的 v-table 里有没有重写Base::doSomething(int)这个特定签名的函数。 - 结果是:没有!
Derived里只有一个doSomething(long),它没有重写doSomething(int)。 - 因此,动态绑定最终还是只能调用到基类
Base的版本。
结论:程序员的本意是让 Derived 对象表现出派生的行为,但由于一个微小的笔误,程序在运行时却悄无声息地调用了基类的版本,这完全违背了多态的初衷,并且这种错误很难被发现。
override 的作用
面试官 (引入 override): 好的,既然我们知道了这个风险。那么,override 关键字就是为了解决这个问题而生的。
请问,如果我们在 Derived 的函数声明后面加上 override,会发生什么?
| |
加上 override 之后,这段代码还能编译通过吗?为什么? override 究竟是向编译器传达了一个怎样的承诺?
回答:
- 加上
override之后,这段代码无法编译通过。 - 因为
override关键字就是在告诉编译器:我这个函数是重写的哦,是对基类中某一个虚函数的重写,请你去找一找,基类中有没有和这个函数签名完全相同的函数,如果没有的话,就是写错了哦。
面试官 (总结与肯定): 你说的完全正确。
override 关键字就像是你和编译器之间的一个契约。你向编译器保证:
“我声明的这个函数,必须重写基类中的一个同名、同参数列表、同
const属性的虚函数。”
编译器在看到 override 后,就会非常严格地进行检查:
- 它会去所有的基类中,查找是否存在一个可以被重写的、签名完全匹配的虚函数。
- 如果找到了,一切正常,编译通过。
- 如果没找到(可能因为函数名拼写错误、参数类型错误、
const属性不匹配等任何原因),编译器就会立刻报错,告诉你违反了契约。
override 的好处:
- 将运行时错误提前到编译时:它把我们之前讨论的那个非常隐蔽的、在运行时才会暴露的多态行为错误,变成了一个在写代码时就能立刻发现的编译错误。这极大地提高了代码的健壮性。
- 提高代码可读性:当别人阅读你的代码时,看到
override,就能立刻明白这个函数是为了实现多态而重写的,而不是一个派生类碰巧新增的同名函数。
黄金法则:
在 C++11 及以后的版本中,只要你的意图是重写一个虚函数,就总是在函数声明的末尾加上
override关键字。
关键字 final
面试官 (引入 final): 好的,你对 override 已经理解得很透彻了。我们来看另一个关键字 final。
final 有两种用法:修饰函数和修饰类。
1. 修饰虚函数:
| |
2. 修饰类:
| |
请问,在这两种情况下,final 关键字分别起到了什么作用?它向编译器传达了怎样的“最终”指令?
回答:final 的作用和它的名字一样,表示“终结”
final修饰虚函数,表示该虚函数无法再被重写了。如果还有该类的派生类试图重写这个虚函数,编译器就会报错。final修饰类,表示该类无法被继承了。如果有该类的派生类出现,编译器就会报错。
面试官 (总结与肯定): 你说的没错,final 关键字的核心作用就是**“终结”**,它为 C++ 的继承体系提供了两个重要的控制点:
1. final 修饰虚函数:终结“重写链”
- 作用:正如你所说,它阻止了任何后续的派生类再次重写这个虚函数。
- 设计意图:这在大型或复杂的类体系中非常有用。当你作为一个基类或中间类的设计者,确定某个虚函数的实现已经是“最终版本”,不希望任何更底层的派生类去修改它的行为时,就可以使用
final。这可以防止意外的重写破坏已有的逻辑,保证了类行为的一致性和稳定性。
2. final 修饰类:终结“继承链”
- 作用:它阻止了任何其他类继承这个类。
- 设计意图:
* 安全性:某些类的设计非常精巧和紧凑,不适合作为基类。开放继承可能会破坏其内部的不变量。将它声明为
final可以防止被误用。 * 性能优化:当编译器知道一个类是final的,并且它调用了一个virtual函数时,编译器就可能进行去虚拟化 (devirtualization) 的优化。因为它确定这个函数调用不存在多态的可能性(没有派生类),所以它可以把运行时的动态绑定,优化成一个更快的、编译时的静态绑定(即直接调用)。
总结
今天我们清晰地辨析了三个重要的 C++ 概念:
- 重载 (Overload) vs 重写 (Override):它们在参数列表、作用域和绑定时机上的核心区别。
override关键字:一个强大的编译时检查工具,用于确保你的重写意图是正确的,避免“意外重载”。final关键字:一个用于“终结”继承或重写的工具,可以增强代码的稳定性和安全性,并可能带来性能优化。