目录
- 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)