目录
8.1 定义
继承:一个类从另一个类继承属性的机制。
直观实例:




Is-A 关系:std::ifstream 是 std::istream,而 std::istream 是 std::ios.
动态多态性:不同类型的对象可能需要相同的接口
可扩展性:继承允许通过创建具有特定属性的子类来扩展类
8.2 继承的实现
以上几何体,都有几何领域维度描述术语,比如表面积,半径,宽度,体积...

.h文件
cpp
class Shape {
public:
// 纯虚函数,在基类中声明,无法实例化;只能在子类中重写
virtual double area() const = 0;
};
// 声明Circle类,它继承于Shape类
class Circle : public Shape {
public:
// 列表初始化构造函数
Circle(double radius): _radius{radius} {};
// 为Circle类重写基类函数area()
double area() const {
return 3.14 * _radius * _radius;
}
private:
// 继承的另一个优点是类变量的封装
double _radius;
};
class Rectangle: public Shape {
public:
// constructor
Rectangle(double height, double width): _height{height}, _width{width} {};
double area() 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) | 在派生类中不可访问 | 在派生类中不可访问 | 在派生类中不可访问 |
私有继承&公有继承

公有继承能更好地模拟"是一个"关系!玩家确实是一个实体,因为它公开地展示了实体的所有功能。
cpp
// 默认私有继承
class Entity {
public:
bool overlapsWith(const Entity& other);
};
class Player : /* private */ Entity {
// Private inheritance:
// - private members of Entity are inaccessible to all
// - public members become private (inaccessible to outside)
};
// 改成公有继承
class Entity {
public:
bool overlapsWith(const Entity& other);
};
class Player : public Entity {
// Public inheritance:
// - private members of Entity are still inaccessible
// - public members become public (accessible to outside)
};
保护继承
受保护的成员对子类可见,但对外部不可见!
cpp
class Entity {
protected:
double x, y, z;
HitBox hitbox;
public:
void update();
void render();
};
cpp
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : protected Base { // protected 继承
public:
void accessBaseMembers() {
publicVar = 10; // OK: Base::publicVar 在 Derived 中变为 protected
protectedVar = 20; // OK: Base::protectedVar 仍是 protected
// privateVar = 30; // 错误: Base::privateVar 不可访问
}
};
int main() {
Derived d;
// d.publicVar = 10; // 错误: Base::publicVar 在 Derived 外是 protected,无法访问
// d.protectedVar = 20; // 错误: protected 成员无法在类外访问
return 0;
}
8.4 菱形问题与虚拟继承
"The Diamond Problem"菱形问题 ,是面向对象编程中多重继承的典型问题。当类 A 同时继承类 B 和类 C,而类 B 和类 C 又均继承自类 D 时,类 A 会包含类 D 的两个副本,可能导致数据冗余或调用歧义,其继承结构形似菱形,故得名。例如:

解决此问题的方法是让 "员工"(Employee)类和 "学生"(Student)类以虚拟继承的方式从 "人"(Person)类继承。
虚拟继承是指,一个派生类(在此处为 "部门负责人"(SectionLeader)类)应当仅包含其基类(在此处为 "人"(Person)类)的单个实例。
cpp
class Student : public virtual Person {
protected:
std::string idNumber; std::string advisor; std::string major;
uint16_t year;
public:
std::string getIdNumber() const; Student(const std::string& name, ...);
std::string getMajor() const; uint16_t getYear() const; void setYear(uint16_t year); void setMajor(const std::string& major);
std::string getAdvisor() const;
void setAdvisor(const std::string& advisor);
This slide is hidden
};
class Employee : public virtual Person {
protected:
double salary;
public:
virtual std::string getRole() const = 0; Employee(const std::string& name);
virtual double getSalary() const = 0;
virtual void setSalary() const = 0;
virtual ~Employee() = default;
};
这要求派生类对基类进行初始化!
8.5 实例展示
8.5.1 实现继承


很多冗余

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

它们有共同的性质:

引入基类:Entity

现在继承了

还有冗余,继续继承!


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

定义通用功能overlapsWith很简单!

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

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

错误案例
我们试一下:
cpp
#include <iostream>
#include <vector>
class Entity {
protected:
double x, y, z;
HitBox hitbox;
public:
virtual void update() {};
virtual void render() {};
};
class Player : public Entity {
double hitpoints = 100;
public:
void damage(double hp) {
hitpoints -= hp;
}
void update() override {
std::cout << "Updating Player!" << std::endl;
}
void render() override {
std::cout << "Rendering Player!" << std::endl;
}
};
class Tree : public Entity {
public:
void update() override {
std::cout << "Updating Tree!" << std::endl;
}
void render() override {
std::cout << "Rendering Tree!" << std::endl;
}
};
class Projectile : public Entity {
private:
double vx, vy, vz;
public:
void update() override {
std::cout << "Updating Projectile!" << std::endl;
}
void render() override {
std::cout << "Rendering Projectile!" << std::endl;
}
};
int main() {
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();
}
}
return 0;
}
解决第一处错误
回想一下,C++ 会按顺序排列对象的字段。C++ 会将子类的成员存放在继承的成员下方!

注意:当你将派生类赋值给基类时,会发生切片现象!
向量中的每个元素都是一个 Entity,因此编译器会调用 Entity::update ()(该函数不执行任何操作),而不是 Player::update ()、Tree::update ()、Projectile::update () 等。

解决方案,用 Entity*
cpp
int main() {
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();
}
}
return 0;
}
指针通过避免复制来保留子类的细节

解决第二处错误
问题:调用哪一个呢?

给定一个指向实体(Entity)的指针,编译器是如何知道该调用哪个方法的呢?
我们应该调用与实体所指向的对象类型相匹配的更新方法。但仅仅一个实体(Entity*)并不能告诉我们任何关于其类型的信息!

编译器默认假设 entity 指向一个 Entity。因为Entity是它唯一能绝对确定任何实体都会支持的类。
注意:对象的编译时类型和运行时类型之间存在差异!
- 在编译时,它被视为一个实体。
- 在运行时,它可以是一个实体或任何子类,例如投射物、玩家等。
我们需要的是动态分派------根据对象的运行时(动态)类型,应该调用(分派)不同的方法!
引入虚函数
cpp
#include <iostream>
#include <vector>
class Entity {
protected:
double x, y, z;
HitBox hitbox;
public:
// 加 virtual
virtual void update() {};
virtual void render() {};
};
class Player : public Entity {
double hitpoints = 100;
public:
void damage(double hp) {
hitpoints -= hp;
}
// 加 override
void update() override {
std::cout << "Updating Player!" << std::endl;
}
void render() override {
std::cout << "Rendering Player!" << std::endl;
}
};
class Tree : public Entity {
public:
void update() override {
std::cout << "Updating Tree!" << std::endl;
}
void render() override {
std::cout << "Rendering Tree!" << std::endl;
}
};
class Projectile : public Entity {
private:
double vx, vy, vz;
public:
void update() override {
std::cout << "Updating Projectile!" << std::endl;
}
void render() override {
std::cout << "Rendering Projectile!" << std::endl;
}
};
int main() {
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();
}
}
return 0;
}
8.5.2 虚函数
- 将函数标记为虚函数可启用动态分派
- 子类可以重写此方法

override 并非必需,但有助于提高可读性!它会检查你是否在重写一个虚方法,而非创建一个新方法。
- 在函数前添加 virtual会给每个对象添加一些元数据。
- 具体来说,它会添加一个指向虚函数表(称为 vtable)的指针(称为 vpointer),该虚函数表说明了对于每个虚方法,应为该对象调用哪个函数。


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

在许多其他语言中,类函数默认是虚函数。而在 C++ 中,你必须主动选择使用虚函数,因为它们的代价更高。
- 这会增加类的内存布局大小。
- 查找虚函数表(vtable)和调用方法会花费更长时间。
在量化金融以及那些纳秒级时间都很重要的行业中,是不使用虚函数的!
8.5.3 纯虚函数
- 包含一个或多个纯虚函数的类是抽象类,它无法被实例化!
- 重写所有纯虚函数会使该类成为具体类!
cpp
classEntity {
public:
virtual voidupdate() = 0;
virtual voidrender() = 0;
};
Entity e;
// 错误:Entity是抽象类,Entity is abstract!
classProjectile
: publicEntity {
public:
void update() override {};
void render() override {};
};
Projectile p;
// 正确:Projectile是具体类,Projectile is concrete
当没有明确的默认实现时,纯虚函数会很有用!

cpp
class Shape {
public:
virtual double volume() = 0;
};
一个 Shape(形状)的默认体积是多少?我们把它标记为纯虚函数,让子类来决定吧!
8.5.4 继承的缺点&组合
庞大的继承树往往速度更慢,且更难理解
- 在电子游戏中,为每种不同的对象类型创建子类的方法在现代游戏引擎中并不常见
- 组合通常更灵活,而且也更合理
- 继承是is-a关系,组合是has关系

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

A car is ****has an engine
cpp
class Car {
Engine* engine;
SteeringWheel* wheel;
Brakes* brakes;
};
class Engine {};
class CombustionEngine : public Engine {};
class GasEngine : public CombustionEngine {};
class DieselEngine : public CombustionEngine{};
class ElectricEngine : public Engine {};
一种使用技巧,指向实现的指针:pImpl(Pointer to implementation)