Effective C 条款36绝不重新定义继承而来的 non-virtual 函数本篇为《Effective C改善程序与设计的 55 个具体做法》读书笔记系列第 36 篇。开篇引言在 C 的面向对象编程中继承和多态是两个核心概念。很多开发者习惯性地认为“子类可以重写父类的任何函数”。然而Scott Meyers 在条款 36 中明确警告绝不重新定义继承而来的 non-virtual 函数。这看似反直觉的建议背后隐藏着 C 对象模型的深层机制。理解这一点对于编写健壮、可维护的 C 代码至关重要。核心问题一个令人困惑的代码示例让我们从一个简单的例子开始看看会发生什么意想不到的事情#includeiostreamclassBase{public:voidfunc(){std::coutBase::func() calledstd::endl;}};classDerived:publicBase{public:voidfunc(){// 警告重新定义了继承而来的 non-virtual 函数std::coutDerived::func() calledstd::endl;}};intmain(){Derived d;Base*pBd;// 基类指针指向派生类对象Derived*pDd;// 派生类指针指向派生类对象pB-func();// 输出Base::func() calledpD-func();// 输出Derived::func() calledreturn0;}令人震惊的结果同一个对象d通过不同类型的指针调用同一个函数却产生了完全不同的行为调用方式实际调用的函数原因pB-func()Base::func()静态绑定指针类型是Base*pD-func()Derived::func()静态绑定指针类型是Derived*这种行为的分裂性正是条款 36 要禁止重新定义 non-virtual 函数的根本原因。原理深度解析静态绑定 vs 动态绑定要理解这个问题我们必须深入 C 的函数调用机制1. 静态绑定Static BindingNon-virtual 函数采用静态绑定也称为早期绑定classBase{public:voidnonVirtualFunc(){/* ... */}// non-virtual};Base*pnewDerived();p-nonVirtualFunc();// 编译器根据 p 的声明类型Base*决定调用 Base::nonVirtualFunc调用哪个函数在编译期就已经确定只与指针/引用的声明类型有关与指针实际指向的对象类型无关2. 动态绑定Dynamic BindingVirtual 函数采用动态绑定也称为晚期绑定classBase{public:virtualvoidvirtualFunc(){/* ... */}// virtual};Base*pnewDerived();p-virtualFunc();// 运行期根据 p 实际指向的对象类型决定调用哪个版本调用哪个函数在运行期才能确定与指针实际指向的对象类型有关通过虚函数表vtable机制实现虚函数表机制简析// 编译器为包含 virtual 函数的类生成虚函数表classBase{public:virtualvoidvf(){/* Base 实现 */}voidnf(){/* Base 实现 */}// 无 vtable 条目};classDerived:publicBase{public:voidvf()override{/* Derived 实现 */}// 覆盖 vtable 条目voidnf(){/* Derived 实现 */}// 与 vtable 无关};机制Non-virtual 函数Virtual 函数绑定时机编译期运行期决定因素指针/引用的声明类型对象的实际类型实现方式直接函数调用通过 vtable 间接调用性能开销无额外开销一次间接寻址为什么这是设计上的矛盾Public 继承的 is-a 关系回顾条款 32public 继承意味着 is-a 关系。如果Derivedpublic 继承自Base那么 “每一个 Derived 对象都是一个 Base 对象”。Non-virtual 函数在设计上代表不变性凌驾于特异性之上classBase{public:voidinvariantBehavior(){// 这个行为对所有 Base 及其派生类都应该是一致的// 它反映了 Base 的不变性}};如果Derived重新定义了invariantBehavior()就会出现逻辑矛盾如果 Derived 确实需要不同的行为说明 “Derived is a Base” 不成立那么不应该使用 public 继承如果 Derived 确实应该是 Base 的一种那么它不应该改变 Base 承诺的不变性如果行为应该因类型而异那么函数应该声明为 virtual代码示例设计矛盾的三难困境#includeiostream// 场景1如果 Base::func 应该反映不变性classAnimal{public:voidbreathe(){// non-virtual所有动物呼吸方式相同std::coutBreathing...std::endl;}};classFish:publicAnimal{public:voidbreathe(){// 错误鱼用鳃呼吸但不应该重写 non-virtualstd::coutBreathing through gills...std::endl;}};// 场景2正确的做法 —— 使用 virtualclassAnimalCorrect{public:virtualvoidbreathe(){std::coutBreathing...std::endl;}virtual~AnimalCorrect()default;};classFishCorrect:publicAnimalCorrect{public:voidbreathe()override{std::coutBreathing through gills...std::endl;}};// 场景3如果行为确实应该统一不需要 virtualclassShape{public:voidprintType()const{// 所有形状都需要打印类型信息方式相同std::coutThis is a shapestd::endl;}};实际应用场景场景 1企业级系统中的账户类#includeiostream#includestringclassAccount{public:// non-virtual所有账户的日志记录方式应该一致voidlogTransaction(conststd::stringinfo)const{std::cout[LOG] Account transaction: infostd::endl;}// virtual不同账户类型计算利息的方式不同virtualdoublecalculateInterest()const0;virtual~Account()default;};classSavingsAccount:publicAccount{public:doublecalculateInterest()constoverride{returnbalance*0.03;// 年利率 3%}// 错误做法// void logTransaction(const std::string info) const {// std::cout [SAVINGS LOG] info std::endl;// }// 这会导致通过 Account* 和 SavingsAccount* 调用产生不同行为private:doublebalance10000.0;};classCheckingAccount:publicAccount{public:doublecalculateInterest()constoverride{return0.0;// 支票账户无利息}private:doublebalance5000.0;};voidprocessAccount(Account*account){// 统一的日志记录non-virtual行为一致account-logTransaction(Interest calculated);// 多态的利息计算virtual行为因类型而异doubleinterestaccount-calculateInterest();std::coutInterest: intereststd::endl;}场景 2游戏引擎中的组件系统classGameComponent{public:// non-virtual所有组件的启用/禁用逻辑相同voidsetEnabled(boolenabled){if(this-enabled!enabled){this-enabledenabled;onEnableStateChanged();}}boolisEnabled()const{returnenabled;}// virtual不同组件的更新逻辑不同virtualvoidupdate(floatdeltaTime)0;virtual~GameComponent()default;protected:// virtual允许派生类响应状态变化virtualvoidonEnableStateChanged(){}private:boolenabledtrue;};classRenderComponent:publicGameComponent{public:voidupdate(floatdeltaTime)override{if(!isEnabled())return;// 渲染逻辑...}// 错误不要重写 setEnabled// void setEnabled(bool enabled) { ... }};常见误区与解决方案误区 1“我只是想加个默认参数”classBase{public:voidfunc(intx10){/* ... */}};classDerived:publicBase{public:voidfunc(intx20){/* ... */}// 错误同时改变了默认参数和隐藏了基类版本};注意这还涉及条款 37绝不重新定义继承而来的缺省参数值的问题。误区 2“我想隐藏基类的实现”classBase{public:voidfunc(){/* 基类实现 */}};classDerived:publicBase{private:voidfunc(){/* 派生类实现 */}// 极度危险不是重写而是隐藏};这不会重写基类函数而是隐藏了它。通过Base*调用的仍然是Base::func()。正确的设计模式需求正确做法说明所有派生类行为一致non-virtual反映不变性不同派生类行为不同virtual支持动态绑定需要扩展基类行为virtual 基类默认实现impure virtual必须强制派生类实现pure virtual接口继承classBase{public:// 情况1不变性 —— non-virtualvoidinvariantOperation(){// 所有派生类共享相同实现}// 情况2可定制行为 —— pure virtualvirtualvoidmustImplement()0;// 情况3有默认实现但可覆盖 —— impure virtualvirtualvoidcustomizableOperation(){// 默认实现}virtual~Base()default;};编译器警告与最佳实践现代编译器通常会对隐藏基类 non-virtual 函数的行为发出警告# GCC/Clang-Woverloaded-virtual# 警告隐藏的虚函数-Wshadow# 警告名称隐藏# MSVC/w14263# 警告隐藏的函数最佳实践清单明确设计意图在声明函数时就想清楚它应该是 virtual 还是 non-virtual使用override关键字C11 引入的override可以帮助捕获错误虽然不能防止 non-virtual 的重定义但可以防止 virtual 函数的签名错误遵循 Liskov 替换原则派生类应该能够替换基类而不改变程序正确性代码审查特别关注派生类中是否有与基类同名的 non-virtual 函数总结核心要点要点说明Non-virtual 函数是静态绑定的调用哪个版本由指针/引用的声明类型决定Public 继承意味着 is-a重定义 non-virtual 函数破坏这一语义Non-virtual 函数代表不变性它应该在继承体系中保持一致需要多态时使用 virtual这是 C 支持运行时多态的正确机制记忆口诀Non-virtual 不覆盖is-a 语义要维护。静态绑定看类型动态绑定看对象。不变性用 non-virtual特异性用 virtual。条款 36 的核心建议绝不重新定义继承而来的 non-virtual 函数。如果你发现需要这样做请重新审视你的继承关系也许不应该使用 public 继承也许这个函数应该声明为 virtual也许你的设计需要重构参考阅读《Effective C》Scott Meyers条款 36《C Primer》Stanley B. Lippman 等关于虚函数和绑定的章节《设计模式》GoF关于继承与组合的探讨系列预告下一篇将深入解析条款 37——绝不重新定义继承而来的缺省参数值探讨静态绑定与动态绑定在参数默认值上的微妙陷阱。如果本文对你有帮助欢迎点赞、收藏、转发有任何问题可以在评论区留言讨论。
Effective C++ 条款36:绝不重新定义继承而来的 non-virtual 函数
发布时间:2026/6/16 13:42:04
Effective C 条款36绝不重新定义继承而来的 non-virtual 函数本篇为《Effective C改善程序与设计的 55 个具体做法》读书笔记系列第 36 篇。开篇引言在 C 的面向对象编程中继承和多态是两个核心概念。很多开发者习惯性地认为“子类可以重写父类的任何函数”。然而Scott Meyers 在条款 36 中明确警告绝不重新定义继承而来的 non-virtual 函数。这看似反直觉的建议背后隐藏着 C 对象模型的深层机制。理解这一点对于编写健壮、可维护的 C 代码至关重要。核心问题一个令人困惑的代码示例让我们从一个简单的例子开始看看会发生什么意想不到的事情#includeiostreamclassBase{public:voidfunc(){std::coutBase::func() calledstd::endl;}};classDerived:publicBase{public:voidfunc(){// 警告重新定义了继承而来的 non-virtual 函数std::coutDerived::func() calledstd::endl;}};intmain(){Derived d;Base*pBd;// 基类指针指向派生类对象Derived*pDd;// 派生类指针指向派生类对象pB-func();// 输出Base::func() calledpD-func();// 输出Derived::func() calledreturn0;}令人震惊的结果同一个对象d通过不同类型的指针调用同一个函数却产生了完全不同的行为调用方式实际调用的函数原因pB-func()Base::func()静态绑定指针类型是Base*pD-func()Derived::func()静态绑定指针类型是Derived*这种行为的分裂性正是条款 36 要禁止重新定义 non-virtual 函数的根本原因。原理深度解析静态绑定 vs 动态绑定要理解这个问题我们必须深入 C 的函数调用机制1. 静态绑定Static BindingNon-virtual 函数采用静态绑定也称为早期绑定classBase{public:voidnonVirtualFunc(){/* ... */}// non-virtual};Base*pnewDerived();p-nonVirtualFunc();// 编译器根据 p 的声明类型Base*决定调用 Base::nonVirtualFunc调用哪个函数在编译期就已经确定只与指针/引用的声明类型有关与指针实际指向的对象类型无关2. 动态绑定Dynamic BindingVirtual 函数采用动态绑定也称为晚期绑定classBase{public:virtualvoidvirtualFunc(){/* ... */}// virtual};Base*pnewDerived();p-virtualFunc();// 运行期根据 p 实际指向的对象类型决定调用哪个版本调用哪个函数在运行期才能确定与指针实际指向的对象类型有关通过虚函数表vtable机制实现虚函数表机制简析// 编译器为包含 virtual 函数的类生成虚函数表classBase{public:virtualvoidvf(){/* Base 实现 */}voidnf(){/* Base 实现 */}// 无 vtable 条目};classDerived:publicBase{public:voidvf()override{/* Derived 实现 */}// 覆盖 vtable 条目voidnf(){/* Derived 实现 */}// 与 vtable 无关};机制Non-virtual 函数Virtual 函数绑定时机编译期运行期决定因素指针/引用的声明类型对象的实际类型实现方式直接函数调用通过 vtable 间接调用性能开销无额外开销一次间接寻址为什么这是设计上的矛盾Public 继承的 is-a 关系回顾条款 32public 继承意味着 is-a 关系。如果Derivedpublic 继承自Base那么 “每一个 Derived 对象都是一个 Base 对象”。Non-virtual 函数在设计上代表不变性凌驾于特异性之上classBase{public:voidinvariantBehavior(){// 这个行为对所有 Base 及其派生类都应该是一致的// 它反映了 Base 的不变性}};如果Derived重新定义了invariantBehavior()就会出现逻辑矛盾如果 Derived 确实需要不同的行为说明 “Derived is a Base” 不成立那么不应该使用 public 继承如果 Derived 确实应该是 Base 的一种那么它不应该改变 Base 承诺的不变性如果行为应该因类型而异那么函数应该声明为 virtual代码示例设计矛盾的三难困境#includeiostream// 场景1如果 Base::func 应该反映不变性classAnimal{public:voidbreathe(){// non-virtual所有动物呼吸方式相同std::coutBreathing...std::endl;}};classFish:publicAnimal{public:voidbreathe(){// 错误鱼用鳃呼吸但不应该重写 non-virtualstd::coutBreathing through gills...std::endl;}};// 场景2正确的做法 —— 使用 virtualclassAnimalCorrect{public:virtualvoidbreathe(){std::coutBreathing...std::endl;}virtual~AnimalCorrect()default;};classFishCorrect:publicAnimalCorrect{public:voidbreathe()override{std::coutBreathing through gills...std::endl;}};// 场景3如果行为确实应该统一不需要 virtualclassShape{public:voidprintType()const{// 所有形状都需要打印类型信息方式相同std::coutThis is a shapestd::endl;}};实际应用场景场景 1企业级系统中的账户类#includeiostream#includestringclassAccount{public:// non-virtual所有账户的日志记录方式应该一致voidlogTransaction(conststd::stringinfo)const{std::cout[LOG] Account transaction: infostd::endl;}// virtual不同账户类型计算利息的方式不同virtualdoublecalculateInterest()const0;virtual~Account()default;};classSavingsAccount:publicAccount{public:doublecalculateInterest()constoverride{returnbalance*0.03;// 年利率 3%}// 错误做法// void logTransaction(const std::string info) const {// std::cout [SAVINGS LOG] info std::endl;// }// 这会导致通过 Account* 和 SavingsAccount* 调用产生不同行为private:doublebalance10000.0;};classCheckingAccount:publicAccount{public:doublecalculateInterest()constoverride{return0.0;// 支票账户无利息}private:doublebalance5000.0;};voidprocessAccount(Account*account){// 统一的日志记录non-virtual行为一致account-logTransaction(Interest calculated);// 多态的利息计算virtual行为因类型而异doubleinterestaccount-calculateInterest();std::coutInterest: intereststd::endl;}场景 2游戏引擎中的组件系统classGameComponent{public:// non-virtual所有组件的启用/禁用逻辑相同voidsetEnabled(boolenabled){if(this-enabled!enabled){this-enabledenabled;onEnableStateChanged();}}boolisEnabled()const{returnenabled;}// virtual不同组件的更新逻辑不同virtualvoidupdate(floatdeltaTime)0;virtual~GameComponent()default;protected:// virtual允许派生类响应状态变化virtualvoidonEnableStateChanged(){}private:boolenabledtrue;};classRenderComponent:publicGameComponent{public:voidupdate(floatdeltaTime)override{if(!isEnabled())return;// 渲染逻辑...}// 错误不要重写 setEnabled// void setEnabled(bool enabled) { ... }};常见误区与解决方案误区 1“我只是想加个默认参数”classBase{public:voidfunc(intx10){/* ... */}};classDerived:publicBase{public:voidfunc(intx20){/* ... */}// 错误同时改变了默认参数和隐藏了基类版本};注意这还涉及条款 37绝不重新定义继承而来的缺省参数值的问题。误区 2“我想隐藏基类的实现”classBase{public:voidfunc(){/* 基类实现 */}};classDerived:publicBase{private:voidfunc(){/* 派生类实现 */}// 极度危险不是重写而是隐藏};这不会重写基类函数而是隐藏了它。通过Base*调用的仍然是Base::func()。正确的设计模式需求正确做法说明所有派生类行为一致non-virtual反映不变性不同派生类行为不同virtual支持动态绑定需要扩展基类行为virtual 基类默认实现impure virtual必须强制派生类实现pure virtual接口继承classBase{public:// 情况1不变性 —— non-virtualvoidinvariantOperation(){// 所有派生类共享相同实现}// 情况2可定制行为 —— pure virtualvirtualvoidmustImplement()0;// 情况3有默认实现但可覆盖 —— impure virtualvirtualvoidcustomizableOperation(){// 默认实现}virtual~Base()default;};编译器警告与最佳实践现代编译器通常会对隐藏基类 non-virtual 函数的行为发出警告# GCC/Clang-Woverloaded-virtual# 警告隐藏的虚函数-Wshadow# 警告名称隐藏# MSVC/w14263# 警告隐藏的函数最佳实践清单明确设计意图在声明函数时就想清楚它应该是 virtual 还是 non-virtual使用override关键字C11 引入的override可以帮助捕获错误虽然不能防止 non-virtual 的重定义但可以防止 virtual 函数的签名错误遵循 Liskov 替换原则派生类应该能够替换基类而不改变程序正确性代码审查特别关注派生类中是否有与基类同名的 non-virtual 函数总结核心要点要点说明Non-virtual 函数是静态绑定的调用哪个版本由指针/引用的声明类型决定Public 继承意味着 is-a重定义 non-virtual 函数破坏这一语义Non-virtual 函数代表不变性它应该在继承体系中保持一致需要多态时使用 virtual这是 C 支持运行时多态的正确机制记忆口诀Non-virtual 不覆盖is-a 语义要维护。静态绑定看类型动态绑定看对象。不变性用 non-virtual特异性用 virtual。条款 36 的核心建议绝不重新定义继承而来的 non-virtual 函数。如果你发现需要这样做请重新审视你的继承关系也许不应该使用 public 继承也许这个函数应该声明为 virtual也许你的设计需要重构参考阅读《Effective C》Scott Meyers条款 36《C Primer》Stanley B. Lippman 等关于虚函数和绑定的章节《设计模式》GoF关于继承与组合的探讨系列预告下一篇将深入解析条款 37——绝不重新定义继承而来的缺省参数值探讨静态绑定与动态绑定在参数默认值上的微妙陷阱。如果本文对你有帮助欢迎点赞、收藏、转发有任何问题可以在评论区留言讨论。