只要子类里出现了和父类同名的函数,父类的那些(哪怕是别的重载版本)就统统"叫不到了"——这叫隐藏(hiding),不是重载,也不是重写。原因是 C++ 的名字查找规则:先在子类的作用域里找这个名字,找到就停,不再往父类里翻。两副解药:① using 父类名::函数名; 把父类的版本"请"进子类作用域;② 父类名::函数名(参数) 明确指名调用。
本节回收 6.5 重载函数 和 6.4 变量作用域:重载是"同一个作用域里同名不同参数",而隐藏是"不同作用域(父子类)里同名"——两者长得像、规则完全不同。这一节还是 11.4 多态的前哨战:本节讲的隐藏是编译期的事(按"指针/引用的类型"找名字),11.4 的虚函数重写是运行期的事(按"对象的真实身份"找函数)。把这两件事分清楚,第十一章就通了。
一、什么叫"隐藏"
先看最干净的现场:
class Base {
public:
void show() { cout << "Base::show()" << endl; }
};
class Derived : public Base {
public:
void show(double d) { cout << "Derived::show(double)" << endl; }
};
int main() {
Derived d;
d.show(3.5); // ✅ 没问题,调 Derived::show(double)
d.show(); // ❌ 编译错!父类的 show() 已经被"藏"起来了
}
d.show(); 的报错:
error: no matching function for call to 'Derived::show()'
note: candidate: void Derived::show(double)
note: candidate expects 1 argument, 0 provided
注意报错里的关键词 candidate: void Derived::show(double)——编译器连提都没提 Base::show()。它压根没去父类里找。
二、为什么参数不同也会被藏(名字查找规则)
C++ 查一个名字(这里是 show)走的是由内向外的过程:
① 先在"当前作用域"(Derived 里)找有没有叫 show 的 → 有!停。
② 不再往 Base 的作用域里看。
③ 然后才拿找到的这堆 show 去做重载决议 → 只有 show(double) 一个能匹配,找不到 show() → 报错。
关键在第①②步:"找名字"和"选哪个重载版本"是两步。
- 第①步(找名字):子类里只要有一个
show,父类所有的show就都不在候选名单里。 - 第③步(选版本):才轮到参数匹配。
所以隐藏与"参数是否相同"无关——同名就藏,一个不留。
三、隐藏 vs 重载 vs 重写(三张面孔,务必分清)
| 名字 | 发生在 | 条件 | 决定时机 |
|---|---|---|---|
| 重载(overload) | 同一个作用域里(同类,或都在全局) | 同名、参数不同(类型/个数/顺序) | 编译期(按实参选) |
| 隐藏(hiding) | 父子类之间(不同作用域) | 同名(不论参数是否相同) | 编译期(按"当前类型"找名字) |
| 重写/覆盖(override) | 父子类之间 | 同名、同参数、且父类是 virtual(11.4) |
运行期(按"对象真实身份") |
一句话串起来:
- 重载:同一个盒子里,同名不同口。
- 隐藏:里层盒子盖住了外层盒子的同名标签——不管你原来有几个口。
- 重写:外层的标签故意让里层去实现,而且运行时才决定用谁的(11.4)。
隐藏和重写长得很像(都是父子之间同名函数),区别只有一个词:
virtual。本节讲的全是"没有 virtual"的情况。
四、解药一:using 声明(推荐)
class Derived2 : public Base {
public:
using Base::show; // ★ 把父类的 show 请进子类作用域
void show(double d) { ... }
};
using Base::show; 的作用:把父类的 show 这个名字引入子类的作用域。于是第①步在子类里就找到了两个 show(父类的 + 自己新写的),它们一起参与重载决议——父类的版本"复活"了。
这是"我确实想扩充一组重载"时的标准写法。
五、解药二:显式指名 父类名::函数名
void callBase() {
Base::show(); // 明确指名:就是要父类那个
}
适用范围:偶尔调一次父类版本时用。如果你希望父类所有版本都能正常用,请用 using。
顺带一提:Base::show() 这个写法,就是 10.1.2 的 ::(作用域解析运算符)——"这个名字属于 Base"。
六、成员变量也会被隐藏
同名的不只是函数,变量同理:
class Base {
public:
int value;
};
class Derived : public Base {
public:
int value; // ← 藏起了 Base::value
void show() {
cout << value; // 子类自己的(内层的赢)
cout << Base::value; // 父类那个(要指名)
cout << this->Base::value; // 另一种指名写法
}
};
注意:这两个 value 是两个不同的变量,各占内存(不像 11.4.3 的虚继承会合成一份)。子类里写 value 拿到的一定是子类那个。
实践建议:别这么写。子类里给成员变量起个不同的名字,比事后
Base::到处指名省钱得多。隐藏变量是 bug 的温床。
七、动手 Demo(含实例03:子类隐藏父类的成员函数)
#include <iostream>
using namespace std;
class Base {
public:
void show() { cout << "Base::show() 无参" << endl; }
void show(int n) { cout << "Base::show(int) —— " << n << endl; }
};
// ── 情形一:子类写了个同名函数,父类两个 show 全被藏 ──
class Derived : public Base {
public:
void show(double d) { cout << "Derived::show(double) —— " << d << endl; }
void callBase() {
Base::show(); // 解药二:指名调用
Base::show(100);
}
};
// ── 情形二:用 using 把父类的 show 请回来 ──
class Derived2 : public Base {
public:
using Base::show; // ★ 解药一
void show(double d) { cout << "Derived2::show(double) —— " << d << endl; }
};
int main() {
Derived d;
d.show(3.5); // ✅ 子类自己的
// d.show(); // ❌ 编译错:no matching function for call to 'Derived::show()'
// d.show(100); // ⚠️ 能编过!但走的是 show(double)(见下方"顺带一坑")
d.callBase(); // 用 Base:: 指名,父类版本照常可用
cout << "── 用 using 引入后 ──" << endl;
Derived2 e;
e.show(3.5); // 子类自己的(double 精确匹配)
e.show(); // ✅ 父类的无参版本回来了
e.show(100); // ✅ 父类的 show(int)(int 精确匹配,赢过 double)
return 0;
}
看到什么算成功:
Derived::show(double) —— 3.5
Base::show() 无参
Base::show(int) —— 100
── 用 using 引入后 ──
Derived2::show(double) —— 3.5
Base::show() 无参
Base::show(int) —— 100
读结果:
| 现象 | 说明 |
|---|---|
d.show(3.5) 走子类版本 |
正常 |
d.show() 编译不过 |
父类的无参 show 被藏了(哪怕它在父类里是"另一个重载") |
d.callBase() 两行都跑通 |
Base:: 指名可以绕过隐藏 |
e.show(100) 走 Base::show(int) |
using 之后,父类版本重新参与重载决议,而且 int 精确匹配赢过 double |
顺带一坑(注释掉的那行 d.show(100);):它能编过——因为 int 可以隐式转成 double,于是它悄悄走了 Derived::show(double),输出 Derived::show(double) —— 100。你的本意很可能是"调父类的 show(int)",结果却被转换规则带偏了。 这种"能编过、结果不对"的错最伤人——这也是为什么建议用 using 而不是靠 Base:: 到处补。
30 秒实验:
1. 把 Derived2 里的 using Base::show; 注释掉,重新编译 → e.show(); 立刻变成 no matching function。
2. 加回来,就好。 一个小小 using,"父类的重载版本全体复活"。
3. 再做一个对照实验:把父类的 show(int) 改成 virtual void show(int)(11.4 的知识),然后再看 d.show(100);——行为不变。因为"名字都没有被找到",虚不虚根本轮不到。这就证明了"隐藏是编译期的事"。
八、常见误区
- "子类写了同名不同参数的函数,就是重载了父类的函数"——不是重载。重载要求"同一个作用域",父子类是两个作用域,所以那是隐藏:父类的所有同名版本都被盖住,一个都调不到。
- "只要参数不同,父类版本就还能用"——错,而且这是本节最大的坑。隐藏跟参数毫无关系:同名就全藏。
- "隐藏和虚函数重写是一回事"——完全不同。隐藏是编译期按"类型"找名字(11.1.5);重写是运行期按"对象真实身份"找函数(11.4)。区别只有一个词:父类那个函数有没有
virtual。 - "隐藏了就只能用
Base::到处补"——有更好的办法:using Base::函数名;一次引入,父类所有重载版本全部回来。这是"我要扩充重载"的标准写法。 - "父类同名成员变量只是被藏起来,还是同一个变量"——不是。它们是两个独立变量、各占内存(这一点和虚继承"合成一份"正好相反,11.4.3)。所以子类里写
value拿到的一定是子类那个,别指望它自动是父类的。 - "隐藏了的函数就彻底没了"——没没。隐藏只是"叫不到",函数还在,用
Base::指名就能调。藏起来的东西 ≠ 不存在。
自测三问(点开对答案)
1. 子类里定义了 `void show(double)`,父类有 `void show()` 和 `void show(int)`,那么 `d.show();` 和 `d.show(100);` 分别会怎样?——`d.show();` **编译错**(`no matching function`,父类无参版本被藏);`d.show(100);` **能编过**,但走的是 `Derived::show(double)`(`int` 隐式转 `double`)——**结果往往不是你要的**。 2. 想让父类的重载版本还能用,怎么办?——两种:**`using Base::show;`**(一次引入所有版本,推荐)或**显式写 `Base::show(参数)`**(偶尔调一次时用)。 3. "隐藏"和"重写(覆盖)"的区别是什么?——**隐藏**:父子类同名函数,**编译期**按当前类型找名字,**和 `virtual` 无关**;**重写**:要求父类函数是 **`virtual`**、签名完全一致,**运行期**按对象的真实类型调函数(11.4)。- ☐ 11.2 重载运算符(下一节)
相关笔记
- 子类显式调用父类的构造函数(含实例02) —— 上一节:给父类构造传参
- 虚函数概述 —— 同样是同名函数,加了
virtual就变成"重写" - 重载函数 —— 重载发生在"同一个作用域"
- 变量作用域 —— 名字查找:由内向外
- 类的声明与定义 ——
::作用域解析运算符 - 🔷 C++基础 · 索引 —— 全课路线图