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

目录

      • [8.1 定义](#8.1 定义)
      • [8.2 继承的实现](#8.2 继承的实现)
      • [8.3 继承类型](#8.3 继承类型)
      • [8.4 菱形问题与虚拟继承](#8.4 菱形问题与虚拟继承)
      • [8.5 实例展示](#8.5 实例展示)

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)

相关推荐
深蓝海拓2 小时前
PySide6从0开始学习的笔记(三) 布局管理器与尺寸策略
笔记·python·qt·学习·pyqt
2401_834517072 小时前
AD学习笔记-34 PCBlogo的添加
笔记·学习
被考核重击3 小时前
浏览器原理
前端·笔记·学习
Lynnxiaowen3 小时前
今天我们继续学习kubernetes内容Helm
linux·学习·容器·kubernetes·云计算
有点。3 小时前
C++ ⼀级 2023 年06 ⽉
开发语言·c++
charlie1145141913 小时前
编写INI Parser 测试完整指南 - 从零开始
开发语言·c++·笔记·学习·算法·单元测试·测试
mmz12073 小时前
前缀和问题2(c++)
c++·算法
冬夜戏雪3 小时前
【java学习日记】【12.14】【12/60】
学习
老华带你飞3 小时前
列车售票|基于springboot 列车售票系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习·spring