🔷 第十一章 继承与派生
📄 一句话概括

只要子类里出现了和父类同名的函数,父类的那些(哪怕是别的重载版本)就统统"叫不到了"——这叫隐藏(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() → 报错。

关键在第①②步:"找名字"和"选哪个重载版本"是两步。

所以隐藏与"参数是否相同"无关——同名就藏,一个不留。

三、隐藏 vs 重载 vs 重写(三张面孔,务必分清)

名字 发生在 条件 决定时机
重载(overload) 同一个作用域里(同类,或都在全局) 同名、参数不同(类型/个数/顺序) 编译期(按实参选)
隐藏(hiding) 父子类之间(不同作用域) 同名(不论参数是否相同) 编译期(按"当前类型"找名字)
重写/覆盖(override) 父子类之间 同名、同参数、且父类是 virtual(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);——行为不变。因为"名字都没有被找到",虚不虚根本轮不到。这就证明了"隐藏是编译期的事"。

八、常见误区

  1. "子类写了同名不同参数的函数,就是重载了父类的函数"——不是重载。重载要求"同一个作用域",父子类是两个作用域,所以那是隐藏:父类的所有同名版本都被盖住,一个都调不到。
  2. "只要参数不同,父类版本就还能用"——错,而且这是本节最大的坑。隐藏跟参数毫无关系:同名就全藏。
  3. "隐藏和虚函数重写是一回事"——完全不同。隐藏是编译期按"类型"找名字(11.1.5);重写是运行期按"对象真实身份"找函数(11.4)。区别只有一个词:父类那个函数有没有 virtual。
  4. "隐藏了就只能用 Base:: 到处补"——有更好的办法:using Base::函数名; 一次引入,父类所有重载版本全部回来。这是"我要扩充重载"的标准写法。
  5. "父类同名成员变量只是被藏起来,还是同一个变量"——不是。它们是两个独立变量、各占内存(这一点和虚继承"合成一份"正好相反,11.4.3)。所以子类里写 value 拿到的一定是子类那个,别指望它自动是父类的。
  6. "隐藏了的函数就彻底没了"——没没。隐藏只是"叫不到",函数还在,用 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)。

相关笔记