C++ / a working model

19 / 80   ·   C++11   ·   约 9 分钟

重载、覆盖、隐藏:Overload / Override / Hiding

先记住这句话

重载在编译期从候选函数中选择签名,覆盖决定虚调用最终执行哪个实现,隐藏则发生在名字查找阶段。三者可以同时出现;使用 override 检查覆盖意图,用 using 恢复需要的基类重载集合。

本篇内容
  1. 先查找,再重载决议
  2. 覆盖匹配虚函数契约
  3. 默认参数不是动态分派的一部分
  4. 运行示例
  5. 动手练习

先查找,再重载决议

重载是同一名字对应多个可选函数,编译器依据实参类型、转换和约束等规则选择最佳匹配。不能仅靠返回类型区分普通函数重载。派生类只要声明某个同名成员,就可能在其作用域中隐藏基类的整个同名集合,即使参数类型完全不同。

因此 Derived::f(double) 不会自动与 Base::f(int) 组成希望中的重载集。需要保留基类候选时,在派生类中加入 using Base::f;。这影响名字查找,不会把基类非虚函数变成虚函数,也不会凭空产生覆盖关系。

覆盖匹配虚函数契约

覆盖要求基类函数是虚函数,并满足参数列表、成员 cv 限定、引用限定等对应要求;返回类型必须一致或满足协变规则,异常说明也不能放宽基类的非抛出承诺。派生类即使省略 virtual,成功覆盖的函数仍然是虚函数。

每次有覆盖意图都写 override,让漏写 const 或写错参数成为编译错误。访问权限不参与判断覆盖:private 虚函数也可以被覆盖。协变只适用于符合条件的类指针或引用,不能把 unique_ptr<Base> 返回值改成 unique_ptr<Derived> 来覆盖。

默认参数不是动态分派的一部分

调用虚函数时,重载决议与默认实参取自调用表达式的静态上下文,然后虚机制再选择最终覆盖者。示例中通过 Base& 调用 value(),取到默认值一,却执行 Derived 的实现;直接用派生对象调用则取默认值二。

显式写 d.Base::value() 会抑制这次调用的虚分派,直接调用基类实现。设计接口时最好不要在覆盖函数上更改默认参数,否则相同对象因观察它的引用类型不同而表现不同,读代码时极易误判。

容易答错的地方

  • 少写 const 可能是新函数加名字隐藏,而不是覆盖;override 能把这类错误提前暴露。
  • using 引入基类函数不等于重新实现它;对虚函数的普通调用仍遵守最终覆盖者规则。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>

struct Base {
    int convert(int) const { return 1; }
    virtual int value(int n = 1) const { return n; }
    virtual ~Base() = default;
};

struct Derived : Base {
    using Base::convert;
    int convert(double) const { return 2; }
    int value(int n = 2) const override { return n * 10; }
};

int main() {
    Derived d;
    const Base& b = d;
    assert(d.convert(3) == 1);
    assert(d.convert(3.5) == 2);
    assert(b.value() == 10);
    assert(d.value() == 20);
    assert(d.Base::value() == 1);
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-overload-override-hide.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

只删除 using Base::convert; 后,d.convert(3) 的结果是什么?为什么?

查看参考答案

结果变为二。派生类的同名声明隐藏基类 convert,名字查找只得到 Derived::convert(double),整数三可转换成 double,所以调用仍合法。编译器不会跨过隐藏关系去选择原本更精确的 Base::convert(int)。恢复 using,或显式调用 d.Base::convert(3),才能选择基类版本。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录