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

Effective C++ 条款32:确定你的 public 继承塑模出 is-a(是一种)关系

Effective C++ 条款32:确定你的 public 继承塑模出 is-a(是一种)关系

public 继承是 C++ 面向对象编程中最核心的机制之一,但也是最常被误用的特性。
本条款将揭示 public 继承的深层含义,帮助你设计出正确的继承体系。


一、问题的提出:继承真的用对了吗?

在 C++ 中,class Derived : public Base这样的代码随处可见。但你是否真正思考过:什么情况下应该使用 public 继承?

来看几个常见的错误示例:

// 错误示例1:企鹅是一种鸟,但企鹅会飞吗?classBird{public:virtualvoidfly(){/* 鸟的飞行实现 */}};classPenguin:publicBird{// 企鹅是一种鸟?// 企鹅不会飞!这里的设计有问题};// 错误示例2:正方形是一种矩形?classRectangle{public:virtualvoidsetWidth(intw){width=w;}virtualvoidsetHeight(inth){height=h;}protected:intwidth,height;};classSquare:publicRectangle{// 正方形是一种矩形?// 正方形的宽和高必须相等,但基类允许独立设置!};

这些看似"理所当然"的继承关系,实际上隐藏着严重的设计缺陷。问题的根源在于:没有正确理解 public 继承的语义


二、is-a 关系的本质

2.1 什么是 is-a 关系?

public 继承意味着 is-a。适用于 base classes 身上的每一件事情,一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。

这句话是理解 public 继承的关键。用更形式化的语言描述,这就是著名的里氏替换原则(Liskov Substitution Principle, LSP)

如果 S 是 T 的子类型,那么程序中所有使用 T 类型对象的地方,都可以无修改地替换为 S 类型对象,而程序的行为保持不变。

2.2 正确的 is-a 关系示例

// 正确的继承:学生是一种人classPerson{public:Person(conststd::string&name,intage):name_(name),age_(age){}virtual~Person()=default;std::stringgetName()const{returnname_;}intgetAge()const{returnage_;}virtualvoidintroduce()const{std::cout<<"我叫"<<name_<<",今年"<<age_<<"岁。\n";}protected:std::string name_;intage_;};classStudent:publicPerson{public:Student(conststd::string&name,intage,conststd::string&school):Person(name,age),school_(school){}voidintroduce()constoverride{std::cout<<"我叫"<<name_<<",今年"<<age_<<"岁,就读于"<<school_<<"。\n";}std::stringgetSchool()const{returnschool_;}private:std::string school_;};// 使用示例:里氏替换原则的完美体现voidgreet(constPerson&person){std::cout<<"欢迎!";person.introduce();}intmain(){Personperson("张三",30);Studentstudent("李四",20,"清华大学");greet(person);// 输出:欢迎!我叫张三,今年30岁。greet(student);// 输出:欢迎!我叫李四,今年20岁,就读于清华大学。// Student 可以完美替代 Person,这就是 is-a 关系}

分析:学生(Student)是一种人(Person),所以学生拥有人的所有属性(姓名、年龄),可以在任何需要人的地方使用。这是 public 继承的正确用法。


三、错误继承关系的深度剖析

3.1 经典反例:正方形与矩形

这是面向对象设计中最著名的陷阱之一:

classRectangle{public:Rectangle(intw,inth):width_(w),height_(h){}virtualvoidsetWidth(intw){width_=w;}virtualvoidsetHeight(inth){height_=h;}intgetWidth()const{returnwidth_;}intgetHeight()const{returnheight_;}intarea()const{returnwidth_*height_;}protected:intwidth_,height_;};classSquare:publicRectangle{public:Square(intside):Rectangle(side,side){}// 正方形的宽和高必须相等!voidsetWidth(intw)override{width_=w;height_=w;// 强制保持相等}voidsetHeight(inth)override{width_=h;// 强制保持相等height_=h;}};

问题分析:

voidprocessRectangle(Rectangle&rect){rect.setWidth(5);rect.setHeight(3);assert(rect.area()==15);// 对于矩形,这个断言成立}intmain(){Squaresq(4);processRectangle(sq);// 传入正方形// sq.setWidth(5) 后,height 也变成了 5// sq.area() == 15 的断言失败!}
问题说明
行为不一致Square 改变了 Rectangle 的行为契约
违反 LSP无法在所有使用 Rectangle 的地方替换为 Square
设计缺陷几何上"正方形是矩形",但程序行为上不是

正确的解决方案:使用组合而非继承,或者重新设计接口。

// 方案1:使用组合classShape{public:virtual~Shape()=default;virtualintarea()const=0;};classRectangle:publicShape{// ... 矩形特有的实现};classSquare:publicShape{// ... 正方形独立的实现,不继承 Rectangleprivate:intside_;};

3.2 经典反例:企鹅与鸟

classBird{public:virtual~Bird()=default;virtualvoideat(){std::cout<<"鸟在吃东西\n";}};classFlyingBird:publicBird{public:virtualvoidfly(){std::cout<<"鸟在飞翔\n";}};classPenguin:publicBird{// 企鹅是一种鸟,但不会飞public:voidswim(){std::cout<<"企鹅在游泳\n";}};// 使用示例voidletBirdFly(Bird&bird){// 如果传入 Penguin,这里会出问题// bird.fly(); // 编译错误!Bird 没有 fly 方法}voidletFlyingBirdFly(FlyingBird&bird){bird.fly();// 安全,因为 FlyingBird 一定会飞}

关键洞察:不是所有鸟都会飞,所以"会飞"不应该成为 Bird 类的接口。正确的做法是将"会飞"提取到 FlyingBird 子类中。


四、is-a 关系的实践检验法

在设计继承关系时,可以通过以下测试来验证 is-a 关系是否成立:

4.1 "是一个"测试

Derived 是一个 Base 吗? - 学生是一个人?是的。 -> public 继承合理 - 正方形是一个矩形?几何上是,但程序行为上不是。 -> 需要重新考虑 - 企鹅是一种鸟?是的。 -> 但"会飞"不是鸟的普遍属性

4.2 替换测试

// 如果以下代码对所有 Derived 对象都应该正确工作,// 那么 Derived public 继承 Base 是合理的voidtestSubstitution(Base&base){// 调用 Base 的所有公有接口base.someMethod();// Derived 对象传入后,行为应该符合预期// 不能出现:// - 抛出意外异常// - 产生不一致的状态// - 违反 Base 的契约}

4.3 需求分析表

关系is-a?建议
Dog -> Animalpublic 继承
Cat -> Animalpublic 继承
Car -> Vehiclepublic 继承
Engine -> Car否(has-a)组合/成员变量
Square -> Rectangle行为上否重新设计或组合
Penguin -> FlyingBird继承自更抽象的 Bird

五、实际应用场景

场景1:GUI 框架中的控件继承

// Qt 风格的控件继承体系classQWidget{public:virtualvoidshow()=0;virtualvoidhide()=0;virtualvoidpaintEvent()=0;virtualQSizesizeHint()const=0;};classQAbstractButton:publicQWidget{public:virtualvoidclick()=0;virtualvoidsetText(constQString&text)=0;virtualQStringtext()const=0;};classQPushButton:publicQAbstractButton{// QPushButton 是一种 QAbstractButton// 所有按钮的属性和行为都适用于 QPushButtonpublic:voidclick()override;voidsetText(constQString&text)override;voidpaintEvent()override;};classQCheckBox:publicQAbstractButton{// QCheckBox 也是一种 QAbstractButton// 但它还有额外的状态:checked/uncheckedpublic:boolisChecked()const;voidsetChecked(boolchecked);voidclick()override;// 切换 checked 状态};

分析:QPushButton is-a QAbstractButtonQCheckBox is-a QAbstractButton。所有按钮的通用行为(点击、设置文本)都适用于这两种具体按钮。

场景2:游戏开发中的角色体系

classGameEntity{public:virtual~GameEntity()=default;virtualvoidupdate(floatdeltaTime)=0;virtualvoidrender()=0;virtualvoidtakeDamage(intamount)=0;Vec3getPosition()const{returnposition_;}voidsetPosition(constVec3&pos){position_=pos;}protected:Vec3 position_;inthealth_=100;boolalive_=true;};classCharacter:publicGameEntity{public:virtualvoidmove(constVec3&direction)=0;virtualvoidattack(GameEntity&target)=0;voidtakeDamage(intamount)override{health_-=amount;if(health_<=0){alive_=false;onDeath();}}protected:virtualvoidonDeath(){}intlevel_=1;};classPlayer:publicCharacter{public:voidupdate(floatdeltaTime)override;voidrender()override;voidmove(constVec3&direction)override;voidattack(GameEntity&target)override;voidgainExperience(intexp);voidequipItem(Item&item);protected:voidonDeath()override{std::cout<<"玩家死亡!游戏结束。\n";}private:intexperience_=0;std::vector<Item>inventory_;};classNPC:publicCharacter{public:voidupdate(floatdeltaTime)override;voidrender()override;voidmove(constVec3&direction)override;voidattack(GameEntity&target)override;voidsetAIBehavior(AIBehavior*behavior);protected:voidonDeath()override{std::cout<<"NPC 死亡。\n";dropLoot();}private:AIBehavior*ai_=nullptr;std::vector<Item>lootTable_;};

分析:

  • Player is-a Character:玩家是一种角色,可以移动、攻击、受到伤害。
  • NPC is-a Character:NPC 也是一种角色,同样可以移动、攻击、受到伤害。
  • 所有对Character的操作都适用于PlayerNPC

场景3:金融系统中的账户类型

classAccount{public:Account(conststd::string&id,doublebalance):accountId_(id),balance_(balance){}virtual~Account()=default;virtualvoiddeposit(doubleamount){balance_+=amount;}virtualboolwithdraw(doubleamount){if(balance_>=amount){balance_-=amount;returntrue;}returnfalse;}doublegetBalance()const{returnbalance_;}std::stringgetAccountId()const{returnaccountId_;}protected:std::string accountId_;doublebalance_;};classSavingsAccount:publicAccount{public:SavingsAccount(conststd::string&id,doublebalance,doublerate):Account(id,balance),interestRate_(rate){}voidapplyInterest(){doubleinterest=balance_*interestRate_;deposit(interest);}private:doubleinterestRate_;};classCheckingAccount:publicAccount{public:CheckingAccount(conststd::string&id,doublebalance,doubleoverdraftLimit):Account(id,balance),overdraftLimit_(overdraftLimit){}boolwithdraw(doubleamount)override{if(balance_+overdraftLimit_>=amount){balance_-=amount;returntrue;}returnfalse;}private:doubleoverdraftLimit_;};// 使用:所有账户都可以统一处理voidprocessMonthlyStatement(Account&account){std::cout<<"账户 "<<account.getAccountId()<<" 余额: "<<account.getBalance()<<"\n";}

六、常见陷阱与最佳实践

6.1 不要混淆 is-a 和 has-a

// 错误:汽车是一种引擎?classCar:publicEngine{// 错误!};// 正确:汽车有一个引擎classCar{private:Engine engine_;// has-a 关系用组合};

6.2 不要混淆 is-a 和 is-implemented-in-terms-of

// 错误:Set 是一个 List?template<typenameT>classSet:publicstd::list<T>{// 危险!// List 允许重复元素,Set 不允许// List 的接口不完全适用于 Set};// 正确:Set 根据 List 实现出来// 使用 private 继承(见条款39)template<typenameT>classSet:privatestd::list<T>{public:voidinsert(constT&item){if(std::find(this->begin(),this->end(),item)==this->end()){this->push_back(item);}}// ...};

6.3 虚析构函数的重要性

classBase{public:// 如果类设计为多态基类,必须有虚析构函数virtual~Base()=default;};classDerived:publicBase{public:~Derived()override{// 清理 Derived 特有的资源}private:std::vector<int>data_;};// 安全的使用方式Base*ptr=newDerived();deleteptr;// 正确:先调用 ~Derived(),再调用 ~Base()

七、总结

要点说明
public 继承 = is-a这是不可违背的语义契约
Liskov 替换原则子类必须能够替换父类而不改变程序行为
行为一致性子类不能弱化父类的行为契约
接口继承子类继承父类的所有公有接口
设计前思考先问"Derived is-a Base?",再写继承代码

请记住:

  • "public 继承"意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。
  • 在设计继承体系之前,先用里氏替换原则检验:所有使用基类的地方,是否都能安全地使用派生类替代?
  • 如果答案是否定的,那么 public 继承不是正确的选择,考虑组合或其他设计模式。

public 继承是 C++ 中最强大的代码复用机制,但也是最危险的。正确使用它,你的代码将优雅而强大;误用它,你将陷入维护的泥潭。始终牢记:is-a 不是语法规则,而是语义契约


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

相关条款:条款33(避免遮掩继承而来的名字)、条款34(区分接口继承和实现继承)、条款38(通过复合塑模出 has-a)

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

相关文章:

  • 119、Sensor 驱动的 I2C 读写封装:Burst Read、连续写入与 Page 寄存器的处理
  • python ide for linux Linux上Python IDE就选Wing Pro?轻量却强大到让你尖叫
  • 如何一键获取九大网盘真实下载地址?LinkSwift全场景指南
  • 法考背诵资料pdf|背诵|资料已整理
  • 一站式音乐聚合革命:如何用智能音源打通全平台壁垒
  • 如何快速搭建个人云游戏平台:Sunshine游戏串流终极完整教程
  • 抖音下载器技术深度解析:从无水印下载到批量处理的完整解决方案
  • 如何彻底解决Windows 11文件资源管理器窗口混乱问题:终极标签管理指南
  • 法考系统强化内部讲义2026|系统强化|资料已整理
  • Sunshine终极指南:如何免费搭建你的个人云游戏服务器
  • 认准报喜鸟【2026街坊私藏】清远管道疏通六强诚信榜:不坐地起价、不暴力施工、30分钟上门、一口价明码 - 极速版本
  • Python PDF处理终极指南:5分钟掌握PyPDF核心功能
  • 法考主观题答题模板|主观题模板|资料已整理
  • MPC8260 MCC全局发送欠载(GUN)错误诊断与恢复实战指南
  • 全志开发环境搭建及编译构建
  • 从SpeexDSP迁移到WebRTC 3A:我们团队踩过的坑和性能提升实测(附代码对比)
  • 2026年6月靠谱的短途叉运公司哪家好推荐,精密设备搬运、工厂整体搬迁、重型设备移位服务商选择指南 - 海棠依旧大
  • 终极指南:如何构建高效的微信好友安全检测系统 - 从传统协议模拟到Hook技术的完整演进
  • AI 辅助代码生成质量评估与自动审查:从“能用就行“到“工程级可靠“
  • 国内制冷快商用冷柜批发厂家实力排行盘点 - 互联网科技品牌测评
  • 医疗数据合规:电子病历作为特殊电子合同的法律认定标准
  • 宴会餐厅厨用设备厂家排行 实测性能与服务对比 - 互联网科技品牌测评
  • 计算机Java毕设实战-基于 SpringBoot 框架的足球俱乐部赛事管理系统的设计与实现 前后端分离架构下足球俱乐部综合管理系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • Java毕设选题推荐:基于 Web 的随机组卷数学题库管理系统的设计与实现 辅助教学的 Web 数学试题智能生成系统【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 2026 年 6 月泰州 GEO/SEO 优化公司实测:十家头部服务商真实转化效果对比 - 936品牌测评网
  • 自助打印机怎么选?2026年主流厂商与场景化方案全解析 - 优质品牌商家
  • 如何高效使用ComfyUI_IPAdapter_plus多图输入:提升AI绘画效果的完整技巧
  • CAD图纸防泄密软件有哪些?盘点六款CAD图纸加密软件,码住
  • 尼康相机推荐哪个品牌的卡 - 资讯速览
  • 使用e-tree开发树形穿梭框