news 2025/12/25 14:27:15

C++基础:Stanford CS106L学习笔记 8 继承

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++基础:Stanford CS106L学习笔记 8 继承

目录

      • 8.1 定义
      • 8.2 继承的实现
      • 8.3 继承类型
        • 私有继承&公有继承
        • 保护继承
      • 8.4 菱形问题与虚拟继承
      • 8.5 实例展示
        • 8.5.1 实现继承
        • 错误案例
          • 解决第一处错误
          • 解决第二处错误
        • 8.5.2 虚函数
        • 8.5.3 纯虚函数
        • 8.5.4 继承的缺点&组合

8.1 定义

继承:一个类从另一个类继承属性的机制。

直观实例:


Is-A 关系:std::ifstream 是 std::istream,而 std::istream 是 std::ios.

动态多态性:不同类型的对象可能需要相同的接口

可扩展性:继承允许通过创建具有特定属性的子类来扩展类

8.2 继承的实现

以上几何体,都有几何领域维度描述术语,比如表面积,半径,宽度,体积…

.h文件

classShape{public:// ​纯虚函数,在基类中声明,无法实例化;只能在子类中重写virtualdoublearea()const=0;};// 声明Circle类,它继承于Shape类classCircle:publicShape{public:// 列表初始化构造函数Circle(doubleradius):_radius{radius}{};// 为Circle类重写基类函数area()doublearea()const{return3.14*_radius*_radius;}private:// 继承的另一个优点是类变量的封装double_radius;};classRectangle:publicShape{public:// constructorRectangle(doubleheight,doublewidth):_height{height},_width{width}{};doublearea()const{return_width*_height;}private:double_width,_height;};

8.3 继承类型

访问权限类型(Type)public(公有继承)protected(保护继承)private(私有继承)默认
示例(Example)class B: public A {…}class B: protected A {…}class B: private A {…}
公有成员(Public Members)在派生类中仍为公有(public)在派生类中变为保护(protected)在派生类中变为私有(private)
保护成员(Protected Members)在派生类中仍为保护(protected)在派生类中仍为保护(protected)在派生类中变为私有(private)
私有成员(Private Members)在派生类中不可访问在派生类中不可访问在派生类中不可访问
私有继承&公有继承

公有继承能更好地模拟“是一个”关系!玩家确实是一个实体,因为它公开地展示了实体的所有功能。

// 默认私有继承classEntity{public:booloverlapsWith(constEntity&other);};classPlayer:/* private */Entity{// Private inheritance:// - private members of Entity are inaccessible to all// - public members become private (inaccessible to outside)};// 改成公有继承classEntity{public:booloverlapsWith(constEntity&other);};classPlayer:publicEntity{// Public inheritance:// - private members of Entity are still inaccessible// - public members become public (accessible to outside)};
保护继承

受保护的成员对子类可见,但对外部不可见!

classEntity{protected:doublex,y,z;HitBox hitbox;public:voidupdate();voidrender();};
classBase{public:intpublicVar;protected:intprotectedVar;private:intprivateVar;};classDerived:protectedBase{// protected 继承public:voidaccessBaseMembers(){publicVar=10;// OK: Base::publicVar 在 Derived 中变为 protectedprotectedVar=20;// OK: Base::protectedVar 仍是 protected// privateVar = 30; // 错误: Base::privateVar 不可访问}};intmain(){Derived d;// d.publicVar = 10; // 错误: Base::publicVar 在 Derived 外是 protected,无法访问// d.protectedVar = 20; // 错误: protected 成员无法在类外访问return0;}

8.4 菱形问题与虚拟继承

“The Diamond Problem”​菱形问题​,是面向对象编程中多重继承的典型问题。当类 A 同时继承类 B 和类 C,而类 B 和类 C 又均继承自类 D 时,类 A 会包含类 D 的两个副本,可能导致数据冗余或调用歧义,其继承结构形似菱形,故得名。例如:

解决此问题的方法是让 “员工”(Employee)类和 “学生”(Student)类以虚拟继承的方式从 “人”(Person)类继承。

虚拟继承是指,一个派生类(在此处为 “部门负责人”(SectionLeader)类)应当仅包含其基类(在此处为 “人”(Person)类)的单个实例。

classStudent:publicvirtualPerson{protected:std::string idNumber;std::string advisor;std::string major;uint16_tyear;public:std::stringgetIdNumber()const;Student(conststd::string&name,);std::stringgetMajor()const;uint16_tgetYear()const;voidsetYear(uint16_tyear);voidsetMajor(conststd::string&major);std::stringgetAdvisor()const;voidsetAdvisor(conststd::string&advisor);This slide is hidden};classEmployee:publicvirtualPerson{protected:doublesalary;public:virtualstd::stringgetRole()const=0;Employee(conststd::string&name);virtualdoublegetSalary()const=0;virtualvoidsetSalary()const=0;virtual~Employee()=default;};

这要求派生类对基类进行初始化!

8.5 实例展示

8.5.1 实现继承


很多冗余

且不方便修改

想象一下,我们想要给每个对象添加一个 overlapsWith 方法,该方法用于检查它是否与另一个对象在空间上重叠。

它们有共同的性质:

引入基类:Entity

现在继承了

还有冗余,继续继承!


继承树定义了 “是一个” 的关系。

定义通用功能overlapsWith很简单!

我们将通过为每种实体重写更新和渲染方法来实现游戏的逻辑。让我们为每种实体类型重写更新和渲染函数吧!

游戏本质上是一系列实体的集合,每帧都会对这些实体进行更新和渲染!

错误案例

我们试一下:

#include<iostream>#include<vector>classEntity{protected:doublex,y,z;HitBox hitbox;public:virtualvoidupdate(){}virtualvoidrender(){};};classPlayer:publicEntity{doublehitpoints=100;public:voiddamage(doublehp){hitpoints-=hp;}voidupdate()override{std::cout<<"Updating Player!"<<std::endl;}voidrender()override{std::cout<<"Rendering Player!"<<std::endl;}};classTree:publicEntity{public:voidupdate()override{std::cout<<"Updating Tree!"<<std::endl;}voidrender()override{std::cout<<"Rendering Tree!"<<std::endl;}};classProjectile:publicEntity{private:doublevx,vy,vz;public:voidupdate()override{std::cout<<"Updating Projectile!"<<std::endl;}voidrender()override{std::cout<<"Rendering Projectile!"<<std::endl;}};intmain(){Player player;Tree tree;Projectile proj;std::vector<Entity>entities{player,tree,proj};while(true){std::cout<<"Rendering frame..."<<std::endl;for(auto&entity:entities){entity.update();entity.render();}}std::vector<Entity*>entities{&player,&tree,&proj};while(true){std::cout<<"Rendering frame..."<<std::endl;for(auto&entity:entities){entity->update();entity->render();}}return0;}
解决第一处错误

回想一下,C++ 会按顺序排列对象的字段。C++ 会将子类的成员存放在继承的成员下方!

注意:当你将派生类赋值给基类时,会发生切片现象!

向量中的每个元素都是一个 Entity,因此编译器会调用Entity::update ()(该函数不执行任何操作),而不是Player::update ()、Tree::update ()、Projectile::update ()等。

解决方案,用​Entity*

intmain(){Player player;Tree tree;Projectile proj;std::vector<Entity*>entities{&player,&tree,&proj};while(true){std::cout<<"Rendering frame..."<<std::endl;for(auto&entity:entities){entity->update();entity->render();}}return0;}

指针通过避免复制来保留子类的细节

解决第二处错误

问题:调用哪一个呢?


给定一个指向实体(Entity)的指针,编译器是如何知道该调用哪个方法的呢?

我们应该调用与实体所指向的对象类型相匹配的更新方法。但仅仅一个实体(Entity*)并不能告诉我们任何关于其类型的信息!

编译器默认假设 entity 指向一个 Entity。因为Entity是它唯一能绝对确定任何实体都会支持的类。

注意:对象的编译时类型和运行时类型之间存在差异!

  • 在编译时,它被视为一个实体。
  • 在运行时,它可以是一个实体或任何子类,例如投射物、玩家等。

我们需要的是​动态分派​——根据对象的运行时(动态)类型,应该调用(分派)不同的方法!

引入虚函数

#include<iostream>#include<vector>classEntity{protected:doublex,y,z;HitBox hitbox;public:// 加 virtualvirtualvoidupdate(){}virtualvoidrender(){};};classPlayer:publicEntity{doublehitpoints=100;public:voiddamage(doublehp){hitpoints-=hp;}// 加 overridevoidupdate()override{std::cout<<"Updating Player!"<<std::endl;}voidrender()override{std::cout<<"Rendering Player!"<<std::endl;}};classTree:publicEntity{public:voidupdate()override{std::cout<<"Updating Tree!"<<std::endl;}voidrender()override{std::cout<<"Rendering Tree!"<<std::endl;}};classProjectile:publicEntity{private:doublevx,vy,vz;public:voidupdate()override{std::cout<<"Updating Projectile!"<<std::endl;}voidrender()override{std::cout<<"Rendering Projectile!"<<std::endl;}};intmain(){Player player;Tree tree;Projectile proj;std::vector<Entity*>entities{&player,&tree,&proj};while(true){std::cout<<"Rendering frame..."<<std::endl;for(auto&entity:entities){entity->update();entity->render();}}return0;}
8.5.2 虚函数
  • 将函数标记为虚函数可启用动态分派
  • 子类可以重写此方法


override 并非必需,但有助于提高可读性!它会检查你是否在重写一个虚方法,而非创建一个新方法。

  • 在函数前添加 virtual会给每个对象添加一些元数据。
  • 具体来说,它会添加一个指向虚函数表(称为 vtable)的指针(称为 vpointer),该虚函数表说明了对于每个虚方法,应为该对象调用哪个函数。



Python 会在其内存占用中存储有关对象类型的额外信息!这使得运行时类型检查成为可能。virtual 有点像 Python。Python 和 C++ 的虚函数都存储特定于类型的信息:

在许多其他语言中,类函数默认是虚函数。而在 C++ 中,你必须主动选择使用虚函数,因为它们的代价更高

  • 这会增加类的内存布局大小。
  • 查找虚函数表(vtable)和调用方法会花费更长时间。

在量化金融以及那些纳秒级时间都很重要的行业中,是不使用虚函数的!

8.5.3 纯虚函数
  • 包含一个或多个纯虚函数的类是抽象类,它无法被实例化!
  • 重写所有纯虚函数会使该类成为具体类!
classEntity{public:virtualvoidupdate()=0;virtualvoidrender()=0;};Entity e;// 错误:Entity是抽象类,Entity is abstract!classProjectile:publicEntity{public:voidupdate()override{};voidrender()override{};};Projectile p;// 正确:Projectile是具体类,Projectile is concrete

当没有明确的默认实现时,纯虚函数会很有用!

classShape{public:virtualdoublevolume()=0;};

一个 Shape(形状)的默认体积是多少?我们把它标记为纯虚函数,让子类来决定吧!

8.5.4 继承的缺点&组合

庞大的继承树往往速度更慢,且更难理解

  • 在电子游戏中,为每种不同的对象类型创建子类的方法在现代游戏引擎中并不常见
  • 组合通常更灵活,而且也更合理
  • 继承是is-a关系,组合是has关系

继承是一种强大的工具,但有时,组合才更有意义!

A car​ is ​has​ an engine

classCar{Engine*engine;SteeringWheel*wheel;Brakes*brakes;};classEngine{};classCombustionEngine:publicEngine{};classGasEngine:publicCombustionEngine{};classDieselEngine:publicCombustionEngine{};classElectricEngine:publicEngine{};

一种使用技巧,指向实现的指针:pImpl(Pointer to implementation)

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

【大模型】-LangChain--stream流式同步异步

文章目录1.同步stream流2.异步astream流3.异步astream流json输出4.异步事件astream_events流5.异步多线程1.同步stream流 import os from langchain_community.chat_models import ChatTongyios.environ["DASHSCOPE_API_KEY"] "sk-秘钥" llm ChatTongyi…

作者头像 李华
网站建设 2025/12/24 12:19:42

兜兜英语每日短语:逃单篇

1. &#x1f37d;️&#x1f4a8; dine and dash&#xff08;吃霸王餐 / 吃完就跑&#xff09; 英文&#xff1a;Nine diners in Chongqing dined and dashed, leaving one person behind. 中文&#xff1a;重庆 9 名食客吃霸王餐后逃单&#xff0c;只留下一人 “背锅”&#x…

作者头像 李华
网站建设 2025/12/24 2:41:13

计算机毕业设计springboot汽车智慧检修系统 基于SpringBoot的智能汽车故障预测与维修管理平台 融合IoT的SpringBoot车辆健康监测与维修决策系统

计算机毕业设计springboot汽车智慧检修系统a93520rj &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。 当汽车从“机械”进化为“数据孤岛”&#xff0c;每一次打火、每一脚刹车都…

作者头像 李华
网站建设 2025/12/24 2:33:16

python3

import osdef traverse_files(path):# 遍历当前路径下的所有文件和文件夹for item in os.listdir(path):# 拼接完整路径item_path os.path.join(path, item)if os.path.isfile(item_path):# 是文件则输出路径print("文件:", item_path)elif os.path.isdir(item_path…

作者头像 李华
网站建设 2025/12/25 2:51:42

【3D图像技术分析与实现】Apple Vision Pro三维成像技术栈深度解析

作为空间计算的标杆产品&#xff0c;Apple Vision Pro的三维成像能力是其实现虚实融合体验的核心基石。它并未依赖单一技术路径&#xff0c;而是通过“多传感器硬件阵列专用芯片算力全栈软件框架”的协同架构&#xff0c;构建了兼具精度、实时性与沉浸感的三维感知系统。本文将…

作者头像 李华
网站建设 2025/12/19 22:10:47

经典算法题详解之统计重复个数(三)

算法我们设计一个哈希表 recall&#xff1a;哈希表 recall 以 s2 字符串的下标 index 为索引&#xff0c;存储匹配至第 s1cnt 个 s1 的末尾&#xff0c;当前匹配到第 s2cnt 个 s2 中的第 index 个字符时&#xff0c; 已经匹配过的 s1 的个数 s1cnt 和 s2 的个数 s2cnt 。我们在…

作者头像 李华