Item36--绝不重新定义继承而来的 non-virtual 函数

1. 现象:行为分裂(Schizophrenic Behavior)

让我们看一个反面教材。假设有一个基类 B 和一个派生类 D。基类中有一个普通函数 mf(非虚函数),但你在派生类中错误地重新定义(隐藏/Shadow)了它:

#include <iostream>

class B {
public:
    void mf() { 
        std::cout << "B::mf()" << std::endl; 
    }
};

class D : public B {
public:
    // 错误!重新定义了继承来的非虚函数
    // 注意:这在C++中被称为隐藏(Hiding),而不是重写(Overriding)
    void mf() { 
        std::cout << "D::mf()" << std::endl; 
    }
};

现在的行为将会非常令人困惑:

D x;            // x 是一个 D 对象

B* pB = &x;     // pB 指向 x (静态类型是 B*)
D* pD = &x;     // pD 指向 x (静态类型是 D*)

// 诡异的现象发生了:
pB->mf();       // 输出: "B::mf()"
pD->mf();       // 输出: "D::mf()"

问题所在: 明明 pBpD 指向的是内存中的同一个对象 x,但调用同一个函数名 mf() 时,结果却不一样。这意味着这个对象的行为取决于你“通过什么类型的窗口(指针)”去观察它。这在软件设计中是不可接受的。


2. 技术层面的解释:静态绑定 vs 动态绑定

为什么会发生这种情况?这是由 C++ 确定函数调用的机制决定的:

  • Virtual 函数(虚函数):动态绑定 (Dynamic Binding) 如果 mf 是虚函数,运行期会根据指针实际指向的对象类型来决定调用哪个函数。如果是这样,上述两个调用都会输出 D::mf()
  • Non-virtual 函数(非虚函数):静态绑定 (Static Binding) 非虚函数在编译期就决定了。编译器只看指针的声明类型(Static Type)。
    • 因为 pB 被声明为 B*,编译器看到 mf 是非虚函数,直接生成调用 B::mf() 的代码。
    • 因为 pD 被声明为 D*,编译器生成调用 D::mf() 的代码。

结论: 当你在派生类中写了一个与基类同名的 non-virtual 函数时,你并没有“重写”它,而是遮盖(Shadow/Hide) 了它。


3. 设计层面的解释:违反 "Is-a" 关系

从面向对象设计的理论高度来看,这条规则更为重要。Item 34 曾讨论过“接口继承”与“实现继承”的区别:

  1. Pure Virtual 函数:继承接口。
  2. Impure Virtual 函数:继承接口 + 提供一份可被覆盖的默认实现。
  3. Non-virtual 函数:继承接口 + 强制继承一份不可修改的实现

逻辑推导:

  • 前提: Public 继承意味着 "Is-a"(是一个)的关系。适用于基类 B 的所有性质,必须同样适用于派生类 D
  • 基类的承诺:B 中声明 mf 为 non-virtual,意味着 B 的设计者在声明:“mf 的行为在该继承体系中是不变的(Invariant),无论你是 B 还是 Dmf 做的事情都应该一模一样。”
  • 派生类的背叛: 如果你在 D 中重新定义了 mf,你实际上是在说:“在这个子类中,这个承诺无效了。”

矛盾产生:

  • 如果你认为 D 需要不同的 mf 实现,那么 mf 在基类中一开始就应该被声明为 virtual
  • 如果你认为 mf 在基类中就应该是 non-virtual,那么 D 就不应该重新定义它。

4. 常见的误区

很多从 Java 或 Python 转到 C++ 的开发者容易犯这个错,因为在 Java/Python 中,所有方法默认都是“虚函数”(动态绑定的),重新定义即意味着重写。

但在 C++ 中,如果你想要多态,必须显式使用 virtual。如果你不使用 virtual,就必须遵守“绝不修改”的约定。

5. 析构函数的特例

虽然本条目讲的是普通成员函数,但析构函数也遵循类似的逻辑(尽管稍有不同)。

  • 如果你希望通过基类指针删除派生类对象,基类析构函数必须是 virtual 的(参见 Item 7)。
  • 如果不这样做,不仅会发生静态绑定导致只调用了基类的析构函数,还会导致资源泄漏

总结

为了保证继承体系的一致性:

  1. 基类的 Non-virtual 函数代表了一种强制性实现(Mandatory Implementation)。
  2. 永远不要在派生类中重新定义它。
posted @ 2025-12-17 20:14  belief73  阅读(2)  评论(0)    收藏  举报