昨天我们聊了虚函数和多态的实现,今天我们来聊一个与它紧密相关,但在概念上又容易混淆的话题:重载 (Overloading)、重写 (Overriding) 和隐藏 (Hiding)。
这三个术语描述了 C++ 中函数名相同但行为不同的几种情况,是面试中考察“你是否能精确理解 C++ 概念”的经典问题。
Overloading 和 Overriding 的区别
面试官: 在 C++ 中,我们经常会遇到函数名相同的情况。你能用自己的话分别解释一下,什么是函数重载 (Overloading) 和函数重写 (Overriding) 吗?它们两者最主要的区别是什么?
回答:Overloading 指的是两个函数同名,但是参数列表不同的情况。
Overriding 指的是两个函数同名、参数也相同(更准确地说,函数签名完全相同),但是一个在派生类中,另一个在基类中的情况。
我认为它们的主要区别是“参数是否相同”,以及“两者之间的关系是并列(Overloading)还是继承(Overriding)”。
你的回答非常出色!直截了当,并且准确地抓住了两者最核心的区别。
- 重载 (Overloading): 关键在于参数列表不同 (参数个数、类型、顺序)。
- 重写 (Overriding): 关键在于继承关系,并且函数签名 (signature) 必须完全相同 (函数名、参数列表、
const属性等都相同)。
你的总结“关系是并列还是继承”也非常形象。重载的函数之间是“兄弟”关系,而重写的函数之间是“父子”关系。
确定多态的时机——编译器?运行期?
面试官 (补充与追问): 你说得很好。我们来把这个概念再深化一下。
重载和重写,它们分别是在哪个阶段被决定的?一个是在编译期 (Compile-time),一个是在运行期 (Run-time)。你觉得哪个对应哪个?为什么?
这可以帮助我们理解它们更深层次的机制差异。
回答:我认为,Overloading 属于静态多态,在编译期就被确定了,因为具有 overloading 关系的两个函数只有名字相同,而函数签名不同,事实上可以被看作两个不同函数,这使得编译器可以分辨他们。(编译器在处理函数调用时,会根据传入的实参类型、数量和顺序,查找应该匹配的函数,这个过程被叫做“名字修饰”)
而对于 overriding 属于动态多态,需要在运行期才能被确定,这和我们昨天讨论的虚函数与多态相关,程序必须通过 v-ptr 和 v-table 才能确定我们调用的到底是哪一个函数。‘
面试官 (总结与肯定): 你说得对极了。
重载 (Overloading) 属于静态多态,是在编译期决定的。
- 编译器在处理函数调用时,会根据你传入的实参类型、数量和顺序,像查字典一样,去查找应该匹配哪个版本的同名函数。这个过程叫做名字修饰 (Name Mangling) 或名称改编。编译器会给每个重载函数一个独特的内部名称,所以对编译器来说,它们就是不同的函数。这个决策过程完全不需要等到程序运行。
重写 (Overriding) 属于动态多态,是在运行期决定的。
- 正如你所联想到的,这必须依赖于虚函数 (virtual) 机制。当通过基类指针或引用调用一个虚函数时,编译器无法在编译期知道这个指针到底指向的是基类对象还是哪个派生类对象。
- 因此,它必须等到程序运行时,通过我们昨天讨论的 v-table 和 v-ptr 机制 (动态绑定),才能确定到底应该调用哪个类重写的版本。
你已经把重载和重写的核心区别(参数列表、作用域、决策时机)都掌握了。
名称隐藏 Name Hiding
面试官 (引入新概念): 好的,那我们来看第三个概念,隐藏 (Hiding),也叫名称隐藏 (Name Hiding)。
请看下面这段代码:
| |
在 main 函数中,我们调用了 d.func(10)。10 是一个 int。
- 如果只看
Derived类,它只有一个func(double),参数不匹配。 - 如果看
Base类,它有一个func(int),参数完美匹配。
那么,你觉得这段代码会编译通过吗?如果能,它会输出什么?为什么?
回答:我不太确定,但我认为上面的代码能够编译通过,并且输出的结果为“Derived:: func (double)”。
首先,这个对象为 Derived 类的,因此它调用的应该是自己类的函数,和基类无关。
而那个 int 类型的 10,在传入时会被隐式地转换为 double 类型,以适配这个 func 的函数调用,因此能正常运行。
我的理解正确吗?
我自己跑了一遍代码,试了下,确实是能通过的。
但是,如果把派生类的函数改成下面这样:
| |
这时候就无法编译通过了,因为 int 类型无法被转换为 string 类型。
具体点说,编译器在 Derived 里找到了 func,但发现参数 int 无法匹配 string,并且因为它已经停止了查找,所以它根本不会去 Base 类里寻找那个完美匹配的 func(int)。
这就是名称隐藏 (Name Hiding):
派生类中一个与基类同名(但参数列表不同)的函数,会隐藏掉基类中所有同名的函数(无论参数列表是否相同)。
如何绕过名称隐藏来调用基类的同名函数?
最后一个问题:
如果我们确实希望在 Derived 的对象上,调用那个被隐藏的 Base::func(int),我们应该怎么写代码?
| |
回答:我似乎见过这种情况,在我印象中需要使用一个名为 super 的函数,来调用基类的对应函数,对吗?但具体的代码细节我记不清了。
好吧,我的回忆出错了。这种 super 关键字在 python 和 Java 等语言中确实存在,但是在 C++中,我们不用 super,而是使用作用域解析运算符,也就是我们常用的 ::。
所以,正确的调用基类函数的方式是:
| |
如果是在 Derived 类的成员函数内部,可以这么写:
| |
如何“反隐藏”?
有时候,我们不希望基类的函数被隐藏,希望派生类的对象技能调用 Derived::func(double),也能调用 Base::func(int),并且根据输入的参数类型自动区分。
这时,我们可以在派生类中使用 using 声明,将基类的同名函数拉到派生类的作用域中:
| |
总结
面试结束
今天我们清晰地区分了三个非常容易混淆的概念:
- 重载 (Overloading): 同一作用域内,函数名相同,参数列表不同。编译期静态绑定。
- 重写 (Overriding): 不同作用域(基类和派生类),函数名、参数列表、
const属性都相同(即函数签名完全相同),且基类函数必须是virtual。运行期动态绑定。 - 隐藏 (Hiding): 派生类函数屏蔽了基类的同名函数。这是 C++ 的作用域查找规则导致的,和
virtual无关。
你今天表现得非常棒,不仅能准确回答基础问题,还能在引导下快速理解更复杂的“隐藏”概念。这些知识点对于写出健壮、可预测的 C++ 代码至关重要。