news 2026/6/14 21:12:04

Effective C++ 条款34:区分接口继承和实现继承

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Effective C++ 条款34:区分接口继承和实现继承

Effective C++ 条款34:区分接口继承和实现继承

public 继承看似简单,实则包含两个可分离的部分:接口继承与实现继承。
理解它们的区别,是设计出优雅继承体系的关键一步。


一、问题的提出:继承的两种含义

在 C++ 中,当我们写下这样的代码时,到底在继承什么?

classBase{public:virtualvoidfunc1()=0;// pure virtualvirtualvoidfunc2();// impure virtualvoidfunc3();// non-virtual};classDerived:publicBase{// Derived 从 Base 继承了什么?};

表面上看,Derived继承了Base的三个函数。但深入思考会发现:

  • func1没有实现,派生类必须自己实现
  • func2有默认实现,派生类可以覆盖也可以不覆盖
  • func3有固定实现,派生类不应该覆盖

这三种函数代表了三种截然不同的继承语义。混淆它们,是继承设计中常见的错误。


二、三种成员函数的继承语义

2.1 Pure Virtual 函数:只继承接口

classShape{public:virtual~Shape()=default;// Pure virtual 函数:只指定接口,不指定实现virtualvoiddraw()const=0;virtualdoublearea()const=0;virtualdoubleperimeter()const=0;};classCircle:publicShape{public:Circle(doubleradius):radius_(radius){}// 必须实现所有 pure virtual 函数voiddraw()constoverride{std::cout<<"绘制圆形,半径="<<radius_<<"\n";}doublearea()constoverride{return3.14159*radius_*radius_;}doubleperimeter()constoverride{return2*3.14159*radius_;}private:doubleradius_;};classRectangle:publicShape{public:Rectangle(doublew,doubleh):width_(w),height_(h){}voiddraw()constoverride{std::cout<<"绘制矩形,"<<width_<<"x"<<height_<<"\n";}doublearea()constoverride{returnwidth_*height_;}doubleperimeter()constoverride{return2*(width_+height_);}private:doublewidth_,height_;};

Pure Virtual 函数的设计意图:

特性说明
接口契约定义派生类必须支持的接口
强制实现派生类必须提供自己的实现,否则无法实例化
抽象基类包含 pure virtual 的类是抽象类,不能创建对象
多态基础为运行时多态提供统一的调用接口

令人意外的事实:C++ 允许为 pure virtual 函数提供定义!

classShape{public:// 声明为 pure virtual,但仍可提供默认实现virtualvoiddraw()const=0;virtualdoublearea()const=0;};// 为 pure virtual 函数提供定义voidShape::draw()const{std::cout<<"默认绘制实现\n";}doubleShape::area()const{return0.0;// 默认面积}classTriangle:publicShape{public:// 可以选择调用基类的默认实现voiddraw()constoverride{Shape::draw();// 调用 pure virtual 的默认实现std::cout<<"三角形自定义绘制\n";}doublearea()constoverride{// 必须提供自己的计算returnbase_*height_/2;}private:doublebase_,height_;};

这种"有定义的 pure virtual 函数"的用途:为派生类提供一个可选的默认实现,但强制派生类显式决定是否使用它。

2.2 Impure Virtual 函数:继承接口和默认实现

classAnimal{public:virtual~Animal()=default;// Impure virtual:继承接口 + 默认实现virtualvoidmakeSound()const{std::cout<<"某种动物叫声\n";// 默认实现}virtualvoidmove()const{std::cout<<"动物在移动\n";// 默认实现}};classDog:publicAnimal{public:// 覆盖默认实现,提供特定行为voidmakeSound()constoverride{std::cout<<"汪汪!\n";}// move() 继承默认实现,不需要覆盖};classCat:publicAnimal{public:voidmakeSound()constoverride{std::cout<<"喵喵~\n";}};classFish:publicAnimal{public:voidmakeSound()constoverride{std::cout<<"...(鱼不会叫)\n";}voidmove()constoverride{std::cout<<"鱼在游泳\n";}};

Impure Virtual 函数的设计意图:

特性说明
接口 + 默认实现派生类继承函数的签名和默认行为
可选覆盖派生类可以选择使用默认实现或提供自己的实现
代码复用为大多数派生类提供通用实现,减少重复代码

2.3 Non-Virtual 函数:继承接口和强制实现

classBase{public:// Non-virtual 函数:接口 + 强制实现voidalgorithm(){// 算法的框架,不允许派生类改变step1();step2();step3();}virtual~Base()=default;protected:// 这些步骤派生类可以定制virtualvoidstep1()=0;virtualvoidstep2()=0;virtualvoidstep3()=0;};// 更典型的例子classClock{public:// 获取当前时间:所有时钟的统一接口,不允许覆盖TimegetCurrentTime()const{returnreadHardwareClock();}// 格式化显示:统一的显示方式std::stringformatTime(constTime&t)const{returnt.toString("YYYY-MM-DD HH:MM:SS");}private:virtualTimereadHardwareClock()const=0;// 硬件相关,由派生类实现};classSystemClock:publicClock{private:TimereadHardwareClock()constoverride{// 读取系统时钟returnTime::now();}};classAtomicClock:publicClock{private:TimereadHardwareClock()constoverride{// 读取原子钟returnfetchAtomicTime();}};

Non-Virtual 函数的设计意图:

特性说明
不变性表示派生类不应该改变的行为
一致性确保所有派生类的某个行为完全一致
静态绑定调用在编译期确定,性能更好

三、三种函数的对比总结

classBase{public:// 1. Pure Virtual:只继承接口virtualvoidinterfaceOnly()=0;// 2. Impure Virtual:继承接口 + 默认实现virtualvoidinterfaceWithDefault(){std::cout<<"默认实现\n";}// 3. Non-Virtual:继承接口 + 强制实现voidinterfaceWithMandatory(){std::cout<<"这是唯一实现,不允许覆盖\n";}};
函数类型继承接口?继承实现?派生类能否覆盖?设计意图
Pure Virtual否(可选项)必须覆盖“你必须实现这个功能”
Impure Virtual是(默认)可选覆盖“你可以使用默认实现,也可以自定义”
Non-Virtual是(强制)不应该覆盖“所有派生类的这个行为必须一致”

四、一个完整的实际案例:游戏 AI 系统

让我们用一个游戏 AI 系统来展示三种函数的正确使用:

#include<iostream>#include<string>#include<vector>#include<memory>// AI 行为基类:定义所有 AI 的通用接口classAIBehavior{public:virtual~AIBehavior()=default;// ========== Pure Virtual:每个 AI 必须有自己的实现 ==========// 评估当前状态并决定下一步行动virtualvoidevaluate()=0;// 执行选定的行动virtualvoidexecute()=0;// 获取 AI 的名称virtualstd::stringgetName()const=0;// ========== Impure Virtual:有默认实现,但可覆盖 ==========// 更新 AI 状态(每帧调用)// 默认实现:先评估,再执行virtualvoidupdate(floatdeltaTime){evaluate();if(hasActionSelected()){execute();}}// 受到伤害时的反应// 默认实现:简单的退缩virtualvoidonDamageTaken(intdamage){std::cout<<getName()<<" 受到 "<<damage<<" 点伤害!\n";health_-=damage;if(health_<=0){onDeath();}}// ========== Non-Virtual:所有 AI 的共同行为,不允许改变 ==========// 获取当前生命值(所有 AI 的生命值计算方式相同)intgetHealth()const{returnhealth_;}// 检查 AI 是否存活(统一的判断逻辑)boolisAlive()const{returnhealth_>0;}// 注册到 AI 管理器(统一的注册流程)voidregisterToManager(AIManager&manager){manager.registerAI(this);onRegistered();}protected:inthealth_=100;boolhasAction_=false;boolhasActionSelected()const{returnhasAction_;}// 派生类可以覆盖的钩子virtualvoidonDeath(){std::cout<<getName()<<" 死亡。\n";}virtualvoidonRegistered(){}};// 具体的 AI 实现:巡逻的守卫classPatrolGuardAI:publicAIBehavior{public:std::stringgetName()constoverride{return"巡逻守卫";}voidevaluate()override{// 检查视野内是否有敌人if(detectEnemy()){selectedAction_=Action::Attack;hasAction_=true;}elseif(shouldPatrol()){selectedAction_=Action::Patrol;hasAction_=true;}else{hasAction_=false;}}voidexecute()override{switch(selectedAction_){caseAction::Attack:std::cout<<"守卫发现敌人,发起攻击!\n";break;caseAction::Patrol:std::cout<<"守卫继续巡逻...\n";break;}}// 覆盖默认的伤害反应:守卫会呼叫支援voidonDamageTaken(intdamage)override{AIBehavior::onDamageTaken(damage);// 先调用默认处理if(isAlive()){std::cout<<"守卫呼叫支援!\n";callForBackup();}}private:enumclassAction{Attack,Patrol};Action selectedAction_;booldetectEnemy(){/* ... */returnfalse;}boolshouldPatrol(){/* ... */returntrue;}voidcallForBackup(){/* ... */}};// 具体的 AI 实现:Boss 怪物classBossAI:publicAIBehavior{public:std::stringgetName()constoverride{return"Boss";}voidevaluate()override{// Boss 有更复杂的决策逻辑if(health_<30){selectedAction_=Action::Enrage;hasAction_=true;}elseif(canUseSpecialAttack()){selectedAction_=Action::SpecialAttack;hasAction_=true;}else{selectedAction_=Action::NormalAttack;hasAction_=true;}}voidexecute()override{switch(selectedAction_){caseAction::Enrage:std::cout<<"Boss 进入狂暴状态!\n";attackPower_*=2;break;caseAction::SpecialAttack:std::cout<<"Boss 释放必杀技!\n";break;caseAction::NormalAttack:std::cout<<"Boss 普通攻击。\n";break;}}// Boss 覆盖默认更新:狂暴时更新频率更高voidupdate(floatdeltaTime)override{if(isEnraged_){// 狂暴时更新两次AIBehavior::update(deltaTime);AIBehavior::update(deltaTime);}else{AIBehavior::update(deltaTime);}}private:enumclassAction{Enrage,SpecialAttack,NormalAttack};Action selectedAction_;intattackPower_=50;boolisEnraged_=false;boolcanUseSpecialAttack(){/* ... */returnfalse;}};// 使用示例classAIManager{public:voidregisterAI(AIBehavior*ai){aiList_.push_back(ai);}voidupdateAll(floatdeltaTime){for(auto*ai:aiList_){if(ai->isAlive()){// Non-virtual,统一的存活检查ai->update(deltaTime);// Virtual,调用各自的更新逻辑}}}private:std::vector<AIBehavior*>aiList_;};

设计分析:

函数类型设计理由
evaluate()/execute()/getName()Pure Virtual每种 AI 的行为都不同,必须各自实现
update()/onDamageTaken()Impure Virtual大多数 AI 使用默认逻辑,但特殊 AI(如 Boss)可以覆盖
getHealth()/isAlive()/registerToManager()Non-Virtual所有 AI 的这些行为应该一致,不允许改变

五、常见陷阱:默认实现的危险

Impure virtual 函数虽然方便,但也存在隐患:

classAirplane{public:virtualvoidfly(){// 默认实现:普通飞机的飞行方式std::cout<<"使用默认飞行算法\n";}};classBoeing747:publicAirplane{// 使用默认的 fly() 实现——合理};classModelC172:publicAirplane{// 使用默认的 fly() 实现——合理};// 危险:新增一种飞机,忘记覆盖 fly()classSpaceShuttle:publicAirplane{// 糟糕!航天飞机不应该使用普通飞机的飞行算法!// 但编译器不会报错,因为 fly() 有默认实现};

解决方案:将接口和默认实现分离

classAirplane{public:// 纯虚函数:只声明接口virtualvoidfly()=0;protected:// 默认实现单独提供,派生类必须显式选择是否使用voiddefaultFly(){std::cout<<"使用默认飞行算法\n";}};classBoeing747:publicAirplane{public:voidfly()override{defaultFly();// 显式选择使用默认实现}};classSpaceShuttle:publicAirplane{public:voidfly()override{// 必须自己实现,无法意外使用默认版本std::cout<<"使用航天飞机专用飞行算法\n";}};

六、设计决策流程图

当你在设计基类时,可以用以下流程决定函数的类型:

设计一个成员函数: │ ├─ 派生类必须提供自己的实现? │ ├─ 是 → 使用 Pure Virtual (= 0) │ └─ 否 → 继续问: │ ├─ 派生类可能需要不同的实现? │ ├─ 是 → 使用 Impure Virtual (提供默认实现) │ └─ 否 → 使用 Non-Virtual │ └─ 是否担心派生类忘记覆盖? ├─ 是 → 使用 Pure Virtual + protected 默认实现 └─ 否 → 使用 Impure Virtual

七、总结

函数类型继承内容使用场景
Pure Virtual仅接口定义派生类必须实现的契约
Impure Virtual接口 + 默认实现提供通用行为,允许特殊情况覆盖
Non-Virtual接口 + 强制实现确保所有派生类行为一致

请记住:

  • 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。
  • pure virtual 函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承。
  • non-virtual 函数具体指定接口继承以及强制性实现继承。

正确区分这三种函数类型,并理解它们背后的设计意图,是创建清晰、健壮、可维护的继承体系的基础。每一个 virtual/non-virtual 的选择,都应该是有意识的设计决策,而不是随意的编码习惯。


参考:《Effective C++》第三版,Scott Meyers 著

相关条款:条款32(确定 public 继承塑模出 is-a 关系)、条款33(避免遮掩继承而来的名字)、条款35(考虑 virtual 函数以外的其他选择)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/14 21:11:49

3步快速上手Citra模拟器:在电脑免费畅玩任天堂3DS游戏

3步快速上手Citra模拟器&#xff1a;在电脑免费畅玩任天堂3DS游戏 【免费下载链接】citra A Nintendo 3DS Emulator 项目地址: https://gitcode.com/GitHub_Trending/ci/citra 想要在电脑上重温《精灵宝可梦XY》、《塞尔达传说&#xff1a;时之笛3D》等经典3DS游戏吗&am…

作者头像 李华
网站建设 2026/6/14 21:06:04

如何高效重建A股千档订单簿:创新架构的实战指南

如何高效重建A股千档订单簿&#xff1a;创新架构的实战指南 【免费下载链接】AXOrderBook A股订单簿工具&#xff0c;使用逐笔行情进行订单簿重建、千档快照发布、各档委托队列展示等&#xff0c;包括python模型和FPGA HLS实现。 项目地址: https://gitcode.com/gh_mirrors/a…

作者头像 李华
网站建设 2026/6/14 21:04:22

CSDN创作128天|从一行代码到国家级工业硬核技术全栈体系

还记得 2026年02月06日&#xff0c;我发布了自己CSDN的第一篇技术博客&#xff1a;《001 一行代码优化科拉兹猜想验证&#xff1a;从 Python 到系统动力学的深度解法》当时只是简单的算法探索、代码优化记录。没人能想到&#xff0c;这一篇小小的代码随笔&#xff0c;会成为一整…

作者头像 李华
网站建设 2026/6/14 20:57:04

Python 高手编程系列三千四百一十九:槽

有一个有趣的特性几乎从未被开发人员使用过&#xff0c;就是槽&#xff08;slots&#xff09;。它允许你使用__slots__ 属性来为指定的类设置一个静态属性列表&#xff0c;并在类的每个实例中跳过__dict__字典的创建过程。它可以为属性很少的类节约内存空间&#xff0c;因为每个…

作者头像 李华