当前位置: 首页 > news >正文

Effective C++ 条款36:绝不重新定义继承而来的 non-virtual 函数

Effective C++ 条款36:绝不重新定义继承而来的 non-virtual 函数

本篇为《Effective C++:改善程序与设计的 55 个具体做法》读书笔记系列第 36 篇。

开篇引言

在 C++ 的面向对象编程中,继承和多态是两个核心概念。很多开发者习惯性地认为:“子类可以重写父类的任何函数”。然而,Scott Meyers 在条款 36 中明确警告:绝不重新定义继承而来的 non-virtual 函数。这看似反直觉的建议背后,隐藏着 C++ 对象模型的深层机制。理解这一点,对于编写健壮、可维护的 C++ 代码至关重要。

核心问题:一个令人困惑的代码示例

让我们从一个简单的例子开始,看看会发生什么意想不到的事情:

#include<iostream>classBase{public:voidfunc(){std::cout<<"Base::func() called"<<std::endl;}};classDerived:publicBase{public:voidfunc(){// 警告:重新定义了继承而来的 non-virtual 函数!std::cout<<"Derived::func() called"<<std::endl;}};intmain(){Derived d;Base*pB=&d;// 基类指针指向派生类对象Derived*pD=&d;// 派生类指针指向派生类对象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 Binding)

Non-virtual 函数采用静态绑定(也称为早期绑定):

classBase{public:voidnonVirtualFunc(){/* ... */}// non-virtual};Base*p=newDerived();p->nonVirtualFunc();// 编译器根据 p 的声明类型(Base*)决定调用 Base::nonVirtualFunc
  • 调用哪个函数在编译期就已经确定
  • 只与指针/引用的声明类型有关
  • 与指针实际指向的对象类型无关
2. 动态绑定(Dynamic Binding)

Virtual 函数采用动态绑定(也称为晚期绑定):

classBase{public:virtualvoidvirtualFunc(){/* ... */}// virtual};Base*p=newDerived();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 关系

回顾条款 32:public 继承意味着 is-a 关系。如果Derivedpublic 继承自Base,那么 “每一个 Derived 对象都是一个 Base 对象”。

Non-virtual 函数在设计上代表不变性凌驾于特异性之上

classBase{public:voidinvariantBehavior(){// 这个行为对所有 Base 及其派生类都应该是一致的// 它反映了 Base 的"不变性"}};

如果Derived重新定义了invariantBehavior(),就会出现逻辑矛盾:

  1. 如果 Derived 确实需要不同的行为:说明 “Derived is a Base” 不成立,那么不应该使用 public 继承
  2. 如果 Derived 确实应该是 Base 的一种:那么它不应该改变 Base 承诺的不变性
  3. 如果行为应该因类型而异:那么函数应该声明为 virtual

代码示例:设计矛盾的三难困境

#include<iostream>// 场景1:如果 Base::func 应该反映"不变性"classAnimal{public:voidbreathe(){// non-virtual:所有动物呼吸方式相同std::cout<<"Breathing..."<<std::endl;}};classFish:publicAnimal{public:voidbreathe(){// 错误!鱼用鳃呼吸,但不应该重写 non-virtualstd::cout<<"Breathing through gills..."<<std::endl;}};// 场景2:正确的做法 —— 使用 virtualclassAnimalCorrect{public:virtualvoidbreathe(){std::cout<<"Breathing..."<<std::endl;}virtual~AnimalCorrect()=default;};classFishCorrect:publicAnimalCorrect{public:voidbreathe()override{std::cout<<"Breathing through gills..."<<std::endl;}};// 场景3:如果行为确实应该统一,不需要 virtualclassShape{public:voidprintType()const{// 所有形状都需要打印类型信息,方式相同std::cout<<"This is a shape"<<std::endl;}};

实际应用场景

场景 1:企业级系统中的账户类

#include<iostream>#include<string>classAccount{public:// non-virtual:所有账户的日志记录方式应该一致voidlogTransaction(conststd::string&info)const{std::cout<<"[LOG] Account transaction: "<<info<<std::endl;}// virtual:不同账户类型计算利息的方式不同virtualdoublecalculateInterest()const=0;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:doublebalance=10000.0;};classCheckingAccount:publicAccount{public:doublecalculateInterest()constoverride{return0.0;// 支票账户无利息}private:doublebalance=5000.0;};voidprocessAccount(Account*account){// 统一的日志记录(non-virtual,行为一致)account->logTransaction("Interest calculated");// 多态的利息计算(virtual,行为因类型而异)doubleinterest=account->calculateInterest();std::cout<<"Interest: "<<interest<<std::endl;}

场景 2:游戏引擎中的组件系统

classGameComponent{public:// non-virtual:所有组件的启用/禁用逻辑相同voidsetEnabled(boolenabled){if(this->enabled!=enabled){this->enabled=enabled;onEnableStateChanged();}}boolisEnabled()const{returnenabled;}// virtual:不同组件的更新逻辑不同virtualvoidupdate(floatdeltaTime)=0;virtual~GameComponent()=default;protected:// virtual:允许派生类响应状态变化virtualvoidonEnableStateChanged(){}private:boolenabled=true;};classRenderComponent:publicGameComponent{public:voidupdate(floatdeltaTime)override{if(!isEnabled())return;// 渲染逻辑...}// 错误:不要重写 setEnabled!// void setEnabled(bool enabled) { ... }};

常见误区与解决方案

误区 1:“我只是想加个默认参数”

classBase{public:voidfunc(intx=10){/* ... */}};classDerived:publicBase{public:voidfunc(intx=20){/* ... */}// 错误!同时改变了默认参数和隐藏了基类版本};

注意:这还涉及条款 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# 警告隐藏的函数

最佳实践清单

  1. 明确设计意图:在声明函数时就想清楚它应该是 virtual 还是 non-virtual
  2. 使用override关键字:C++11 引入的override可以帮助捕获错误(虽然不能防止 non-virtual 的重定义,但可以防止 virtual 函数的签名错误)
  3. 遵循 Liskov 替换原则:派生类应该能够替换基类而不改变程序正确性
  4. 代码审查:特别关注派生类中是否有与基类同名的 non-virtual 函数

总结

核心要点

要点说明
Non-virtual 函数是静态绑定的调用哪个版本由指针/引用的声明类型决定
Public 继承意味着 is-a重定义 non-virtual 函数破坏这一语义
Non-virtual 函数代表不变性它应该在继承体系中保持一致
需要多态时使用 virtual这是 C++ 支持运行时多态的正确机制

记忆口诀

Non-virtual 不覆盖,is-a 语义要维护。
静态绑定看类型,动态绑定看对象。
不变性用 non-virtual,特异性用 virtual。

条款 36 的核心建议

绝不重新定义继承而来的 non-virtual 函数。如果你发现需要这样做,请重新审视你的继承关系:

  1. 也许不应该使用 public 继承
  2. 也许这个函数应该声明为 virtual
  3. 也许你的设计需要重构

参考阅读:

  • 《Effective C++》Scott Meyers,条款 36
  • 《C++ Primer》Stanley B. Lippman 等,关于虚函数和绑定的章节
  • 《设计模式》GoF,关于继承与组合的探讨

系列预告:下一篇将深入解析条款 37——绝不重新定义继承而来的缺省参数值,探讨静态绑定与动态绑定在参数默认值上的微妙陷阱。


如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。

http://www.rkmt.cn/news/1535492.html

相关文章:

  • 【Kafka源码解读和使用指南】第85篇:Kafka监控系统搭建实战——Prometheus+Grafana+告警全套方案
  • Windows上运行iOS应用的终极秘籍:3步打造跨平台模拟环境
  • 安康市2026年奢侈品手表包包回收门店权威测评:这五家店铺回收价格最高 - 千叶啊
  • 特征方程:数据科学中被忽视的矩阵健康诊断仪
  • 软考软件设计师备考全攻略:从知识体系构建到实战案例分析
  • Equalizer APO终极指南:3步免费打造专业级音效系统
  • pearOS NiceCore 系统介绍与完整安装部署教程
  • 4个创新场景应用:一站式3D模型可视化解决方案深度实战
  • Effective C++ 条款37:绝不重新定义继承而来的缺省参数值
  • 3步解锁鼠标真实性能:免费开源测试工具完全指南
  • Mesh Navigation未来展望:3D导航技术发展趋势分析
  • 从意图驱动到AI自洽:构建下一代智能网络的核心架构与实践
  • 淮安市闲置爱马仕、劳力士变现指南:奢侈品手表包包回收门店实地测评 - 开始就结束
  • 计算机Java毕设实战-基于 Spring Cloud 的 B2C 电子商城系统研发与实践 分布式微服务架构下电商交易平台【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • ComfyUI-WanVideoWrapper:AI视频创作的创新工具箱与工作流优化指南
  • 结婚以后,网络工程师最该补的课,不是技术,是安排
  • M3U8视频下载新体验:告别复杂命令行,一键轻松搞定流媒体视频
  • 变分自编码器(VAE)原理与PyTorch实战:构建可解释隐空间
  • 告别文献阅读的“窗口切换地狱“:Zotero PDF Preview让你效率提升3倍的秘密武器
  • 终极M3U8视频下载器:告别命令行,一键下载流媒体视频
  • 进程优先级与调度机制
  • SAP-ABAP:SAP表与视图数据一致性方案:锁机制、逻辑校验与变更审计
  • 如何快速上手Dolphin-2.9.3-mistral-7B-32k:5步安装部署教程
  • Learn Harness Engineering常见问题解答:解决你在使用过程中的所有疑惑
  • ImageStrike:一站式解决18种图像隐写挑战的CTF安全工具
  • 8大网盘直链下载助手:免费解决网盘限速的终极指南
  • 2026年 福州房屋拆除公司推荐榜单:专业打墙/厂房拆除/整厂设备拆除及墙体切割拆除服务口碑精选 - 品牌发掘
  • 如何在浏览器中免费查看和测量3D模型?在线3D查看器完整指南
  • VirtualMotionCapture与LIV集成:创建专业级MR合成视频的完整指南
  • ComfyUI完整指南:从零开始掌握AI创作的可视化工作流