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 函数以外的其他选择)