Item33--避免遮掩继承而来的名称

1. 核心现象:名称遮掩 (Name Hiding)

一句话总结:在 C++ 中,子类中的名称会遮掩(Hide)父类中的同名名称,无论参数列表是否相同。

这是由 C++ 的名称查找规则决定的:

  1. 编译器看到一个函数调用(如 d.mf1(5))。
  2. 它从最内层的作用域(子类)开始查找名为 mf1 的东西。
  3. 一旦在子类找到了名为 mf1 的东西,查找就停止了。
  4. 编译器此时完全不关心参数是否匹配,也不关心父类里有没有更合适的重载版本。

问题代码演示

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) 是一把 瑞士军刀,它有两个功能(两个重载函数):

  1. 刀片mf1()
  2. 螺丝刀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) 的妙处

为了只暴露螺丝刀,我们要手动做一个“按钮”。

这就叫 转交函数: 我在 Derivedpublic 区域专门写一个函数,长得跟螺丝刀一样。当用户按这个按钮时,我在内部帮他调用瑞士军刀的螺丝刀功能。

我们再看一遍代码:

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 的私有内容,我帮用户转交这个调用
    }
};

发生了什么?

  1. 对于外部用户 (main)
    • 他们看 Derived,只看到了一个 mf1(int)
    • 他们看不到 mf1()(无参版本),因为你没写转交函数,而且因为是 Private 继承,基类的那个版本对外部是隐藏的。
    • 目的达成:只开放了部分功能。
  2. 对于编译器
    • 用户调 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
posted @ 2025-12-20 21:54  belief73  阅读(1)  评论(0)    收藏  举报