Item33--避免遮掩继承而来的名称
1. 核心现象:名称遮掩 (Name Hiding)
一句话总结:在 C++ 中,子类中的名称会遮掩(Hide)父类中的同名名称,无论参数列表是否相同。
这是由 C++ 的名称查找规则决定的:
- 编译器看到一个函数调用(如
d.mf1(5))。 - 它从最内层的作用域(子类)开始查找名为
mf1的东西。 - 一旦在子类找到了名为
mf1的东西,查找就停止了。 - 编译器此时完全不关心参数是否匹配,也不关心父类里有没有更合适的重载版本。
问题代码演示
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
void mf2();
void mf2(double);
// ...
};
class Derived : public Base {
public:
// 这里的 mf1 会遮掩 Base::mf1() 和 Base::mf1(int)
virtual void mf1();
// 这里的 mf2 会遮掩 Base::mf2() 和 Base::mf2(double)
void mf2();
};
int main() {
Derived d;
int x = 10;
d.mf1(); // 没问题,调用 Derived::mf1
d.mf1(x); // ❌ 错误!Derived::mf1 不接受参数。
// 尽管 Base 有 mf1(int),但它被 Derived::mf1 遮掩了,编译器看不见。
d.mf2(); // 没问题,调用 Derived::mf2
d.mf2(3.5);// ❌ 错误!Derived::mf2 不接受参数。
// Base::mf2(double) 被遮掩了。
}
为什么 C++ 要这样设计? 这是为了防止“意外的继承”。如果在库的更新中,父类加了一个新的重载版本(比如接受 double),而子类原本只处理 int。C++ 认为,如果子类自己定义了同名函数,那么它应该完全接管这个名称的解释权,避免父类的新函数意外截获了本该由子类处理的调用(或者是为了避免类型转换带来的意外行为)。
2. 解决方案:使用 using 声明
在 public 继承(IS-A 关系)中,我们通常希望子类继承父类所有的接口。如果子类重写了其中一个重载版本,我们通常希望其他的版本依然可用。
解法: 使用 using 声明式,将父类的名称“拽”回子类的作用域中。
修正后的代码
class Derived : public Base {
public:
// 让 Base 中所有名为 mf1 和 mf2 的东西在 Derived 作用域内可见
using Base::mf1;
using Base::mf2;
virtual void mf1(); // 重写无参数版本
void mf2(); // 重新定义无参数版本
};
int main() {
Derived d;
int x = 10;
d.mf1(); // ✅ 调用 Derived::mf1
d.mf1(x); // ✅ 调用 Base::mf1(int) —— 它可以被看见了!
d.mf2(); // ✅ 调用 Derived::mf2
d.mf2(3.5);// ✅ 调用 Base::mf2(double)
}
这就实现了我们预期的行为:子类特化了部分功能,但同时也继承了父类的其他重载版本。
3. 特殊场景:Private 继承与转交函数 (Forwarding Functions)
1. 核心比喻:瑞士军刀 vs. 遥控器
想象 基类 (Base) 是一把 瑞士军刀,它有两个功能(两个重载函数):
- 刀片:
mf1() - 螺丝刀:
mf1(int)
场景 A:Public 继承(我是你爸爸)
如果是 Public 继承,你的子类也是一把瑞士军刀。用户拿到子类,理所当然应该既能用刀片,也能用螺丝刀。
- 问题:如果被遮掩了,我们用
using,相当于大喊一声:“把爸爸所有的功能都露出来!”于是刀片和螺丝刀都能用了。
场景 B:Private 继承(我是个黑盒子)
Private 继承 的意思是:我内部偷偷用了瑞士军刀来制作这个产品,但我不是瑞士军刀。
- 比如你做了一个 “自动拧螺丝机” (Derived)。
- 你的机器内部藏了一把瑞士军刀(Base),用来实现拧螺丝的功能。
现在的需求是: 作为“自动拧螺丝机”,你只希望用户能用 螺丝刀 (mf1(int)) 的功能。 你绝不希望用户能把手伸进来用那把 刀片 (mf1()),因为那太危险了,也不符合你机器的用途。
2. 为什么 using 在这里不行?
如果你在这个场景下用了 using Base::mf1;:
class Derived : private Base {
public:
using Base::mf1; // ❌ 完蛋了!
};
这就相当于你在这个机器上开了一个大洞,把里面的瑞士军刀彻底暴露给了用户。用户不仅能用螺丝刀,也能把刀片拔出来伤人(调用 mf1())。这就破坏了你只想暴露特定接口的初衷。
3. 转交函数 (Forwarding Function) 的妙处
为了只暴露螺丝刀,我们要手动做一个“按钮”。
这就叫 转交函数: 我在 Derived 的 public 区域专门写一个函数,长得跟螺丝刀一样。当用户按这个按钮时,我在内部帮他调用瑞士军刀的螺丝刀功能。
我们再看一遍代码:
class Base {
public:
virtual void mf1(); // 刀片
virtual void mf1(int); // 螺丝刀
};
class Derived : private Base { // 私有继承:瑞士军刀被藏在内部,外部默认全都看不见
public:
// 【关键点】:我手动写一个同名的函数,作为“代理”
virtual void mf1(int x) {
Base::mf1(x); // 只有我(Derived)能看见 Base 的私有内容,我帮用户转交这个调用
}
};
发生了什么?
- 对于外部用户 (main):
- 他们看
Derived,只看到了一个mf1(int)。 - 他们看不到
mf1()(无参版本),因为你没写转交函数,而且因为是 Private 继承,基类的那个版本对外部是隐藏的。 - 目的达成:只开放了部分功能。
- 他们看
- 对于编译器:
- 用户调
d.mf1(10)-> 找到了 Derived 自己的mf1(int)-> 内部调用Base::mf1(10)-> 成功。 - 用户调
d.mf1()-> 在 Derived 只有带参数的版本 -> 报错(或者根本找不到)。
- 用户调
总结
- Public 继承 + 遮掩:我想继承所有功能 -> 使用
using声明式(一键全开)。 - Private 继承 + 遮掩:我只想暴露特定功能 -> 使用 转交函数(精准开放)。
4. 总结与现代启示
- 黄金法则:如果你在子类中定义了一个函数,其名称与父类中的函数同名,你就会遮掩父类中所有该名称的函数(无论参数、虚函数属性如何)。
- Public 继承:为了遵循 IS-A 关系,请务必使用
using Base::FunctionName;让被遮掩的重载版本重见天日。 - Modern C++ (
override):- 虽然 C++11 引入了
override关键字来帮助检查虚函数重写的正确性,但它不能解决名称遮掩问题。 - 如果你写了
void mf1() override;,这只是告诉编译器你要重写虚函数,但如果父类还有mf1(int),它依然会被遮掩。你依然需要using。
- 虽然 C++11 引入了
浙公网安备 33010602011771号