测验与小总结

小结

继承让我们能够在两个对象之间建立“is-a”(是一个)的关系。被继承的对象称为父类(parent class)、基类(base class)或超类(superclass);执行继承的对象称为子类(child class)、派生类(derived class)或子类(subclass)。

当派生类继承自基类时,派生类会获得基类的全部成员。

派生类对象的构造顺序如下(更详细地说):

  1. 为派生类分配内存(需容纳基类部分与派生类部分)。
  2. 调用合适的派生类构造函数。
  3. 首先使用合适的基类构造函数构造基类对象。若未显式指定基类构造函数,则使用默认构造函数。
  4. 派生类构造函数的初始化列表初始化派生类自己的成员。
  5. 执行派生类构造函数体。
  6. 将控制权返回给调用者。

析构顺序与之相反:从最派生到最基类依次析构。

C++ 有三种访问说明符:public、private、protected。protected 允许所属类本身、友元以及派生类访问该成员,但禁止其他外部访问。

类可以以 public、private 或 protected 方式继承另一个类;几乎总是采用 public 继承。

下表列出了基类成员的访问说明符与各种继承方式组合后的结果:

基类中的访问说明符public 继承后的访问说明符private 继承后的访问说明符protected 继承后的访问说明符
PublicPublicPrivateProtected
Private不可访问不可访问不可访问
ProtectedProtectedPrivateProtected

派生类可以:

  • 增加新函数;
  • 改变基类已有函数的行为;
  • 改变继承成员的访问级别;
  • 隐藏功能。

多重继承允许一个派生类从多个父类继承成员。除非替代方案会导致更复杂的代码,否则应避免多重继承。

测验时间

问题 #1

对下列程序,请判断其输出,或指出无法编译的原因。本题旨在通过阅读代码完成,请勿实际编译(否则答案过于简单)。

a)

#include <iostream>

class Base {
public:
    Base() { std::cout << "Base()\n"; }
    ~Base() { std::cout << "~Base()\n"; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived()\n"; }
    ~Derived() { std::cout << "~Derived()\n"; }
};

int main() {
    Derived d;
    return 0;
}

b)

// 代码与a相同
int main() {
    Derived d;
    Base b;
    return 0;
}

提示:局部变量的销毁顺序与定义顺序相反。

c)

#include <iostream>

class Base {
private:
    int m_x {};
public:
    Base(int x) : m_x{ x } { std::cout << "Base()\n"; }
    ~Base() { std::cout << "~Base()\n"; }
    void print() const { std::cout << "Base: " << m_x << '\n'; }
};

class Derived : public Base {
public:
    Derived(int y) : Base{ y } { std::cout << "Derived()\n"; }
    ~Derived() { std::cout << "~Derived()\n"; }
    void print() const { std::cout << "Derived: " << m_x << '\n'; }
};

int main() {
    Derived d{ 5 };
    d.print();
    return 0;
}

d)

class Base {
protected:          // 仅此处改为 protected
    int m_x {};
    // 其余同 c
};
// Derived 与 main() 同 c

e)

class D2 : public Derived {
public:
    D2(int z) : Derived{ z } { std::cout << "D2()\n"; }
    ~D2() { std::cout << "~D2()\n"; }
    // 注意:此处无 print() 函数
};

int main() {
    D2 d{ 5 };
    d.print();
    return 0;
}

问题 #2

a) 编写 Apple 类与 Banana 类,均派生自公共的 Fruit 类。Fruit 应有两成员:name(名称)与 color(颜色)。

要求以下程序可运行:

int main() {
    Apple a{ "red" };
    Banana b{};
    std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
    std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
    return 0;
}

并输出: My apple is red. My banana is yellow.

b) 在上题程序中新增 GrannySmith 类,继承自 Apple。

要求以下程序可运行:

int main() {
    Apple a{ "red" };
    Banana b;
    GrannySmith c;
    // 输出示例略
}

并输出: My apple is red. My banana is yellow. My granny smith apple is green.

问题 #3(挑战题)

本题为难度较高、篇幅较长的测验。我们将编写一个简单的打怪游戏:玩家在死亡或达到 20 级之前尽可能多地收集黄金。

程序包含 3 个类:Creature、Player、Monster。Player 与 Monster 均继承自 Creature。

a) 首先创建 Creature 类。Creature 具有 5 个属性:名称(std::string)、符号(char)、生命值(int)、每次攻击造成的伤害(int)、携带的黄金量(int)。将它们实现为类成员。为每个属性编写 getter。再添加三个函数:

  • void reduceHealth(int) 减少 Creature 的生命值;
  • bool isDead() 当生命值 ≤ 0 时返回 true;
  • void addGold(int) 向 Creature 添加黄金。

要求以下程序可运行:

int main() {
    Creature o{ "orc", 'o', 4, 2, 10 };
    o.addGold(5);
    o.reduceHealth(1);
    std::cout << "The " << o.getName() << " has " << o.getHealth()
              << " health and is carrying " << o.getGold() << " gold.\n";
    return 0;
}

输出: The orc has 3 health and is carrying 15 gold.

b) 接下来创建 Player 类。Player 继承自 Creature,并额外拥有一个成员——玩家等级,初始为 1。 玩家由用户输入自定义名称,符号为 ‘@’,初始生命 10,初始伤害 1,黄金 0。 编写 levelUp() 函数,使玩家等级与伤害各 +1;编写等级 getter;编写 hasWon() 函数,当玩家达到 20 级时返回 true。 编写新的 main() 函数,提示用户输入姓名,并产生如下输出: Enter your name: Alex Welcome, Alex. You have 10 health and are carrying 0 gold.

c) 接下来是 Monster 类。Monster 同样继承自 Creature,无新增非继承成员变量。 先编写一个空的 Monster 类继承自 Creature;然后在 Monster 类内部添加枚举 Type,枚举值为 dragon、orc、slime,以及 max_types(后续有用)。

d) 每种 Monster 类型拥有不同的名称、符号、初始生命、伤害与黄金。下表给出各类型属性:

类型名称符号生命伤害黄金
dragondragonD204100
orcorco4225
slimeslimes1110

下一步编写 Monster 构造函数,使其接受一个 Type 枚举参数,并根据该类型创建具有相应属性的 Monster。 由于所有怪物属性均为预定义(非随机或定制),可使用查找表。查找表为 C 风格数组 Creature monsterData[],按 Type 索引即可返回对应 Creature。 该表为 Monster 专属,可在 Monster 类内定义为 static inline Creature monsterData[] { },并用 Creature 元素初始化。 Monster 构造函数只需调用 Creature 的复制构造函数,并传入 monsterData 中对应的 Creature 即可。

要求以下程序可通过编译:

int main() {
    Monster m{ Monster::Type::orc };
    std::cout << "A " << m.getName() << " (" << m.getSymbol() << ") was created.\n";
    return 0;
}

并输出: A orc (o) was created.

e) 最后,向 Monster 添加静态函数 getRandomMonster()。该函数应随机选取 0 至 max_types-1 的整数,并返回对应 Type 的 Monster(按值返回,需要将 int static_cast 为 Type)。 在课程《全局随机数(Random.h)》提供了可用的随机数代码。

要求以下 main 函数可运行:

int main() {
    for (int i{ 0 }; i < 10; ++i) {
        Monster m{ Monster::getRandomMonster() };
        std::cout << "A " << m.getName() << " (" << m.getSymbol() << ") was created.\n";
    }
    return 0;
}

输出应为随机结果。

f) 现在编写游戏主逻辑!

游戏规则:

  • 玩家一次遭遇一只随机生成的怪物。
  • 每次遭遇,玩家可选择 (R) 逃跑或 (F) 战斗。
  • 若选择逃跑,有 50% 概率成功。
    • 成功:无负面效果,进入下一次遭遇。
    • 失败:怪物免费攻击一次,玩家再次选择行动。
  • 若选择战斗:
    1. 玩家先攻击,怪物生命减少玩家伤害值。
    2. 若怪物死亡,玩家获得怪物携带的黄金,玩家升级(等级与伤害 +1)。
    3. 若怪物未死亡,怪物反击,玩家生命减少怪物伤害值。
  • 游戏结束条件:玩家死亡(失败)或达到 20 级(胜利)。
  • 玩家死亡时,告知其等级与黄金数量;胜利时告知胜利与黄金数量。

示例游戏流程(略,见原文)。

提示:创建 4 个函数:

  • main():负责游戏初始化(创建 Player)与主循环。
  • fightMonster():处理玩家与单只 Monster 的战斗,询问玩家行动,处理逃跑或战斗。
  • attackMonster():处理玩家攻击怪物及升级。
  • attackPlayer():处理怪物攻击玩家。

g) 附加题: 读者 Tom 的剑不够锋利,无法击败巨龙。请帮他实现以下不同尺寸的药水:

类型小型效果中型效果大型效果
Health+2 生命+2 生命+5 生命
Strength+1 伤害+1 伤害+1 伤害
Poison-1 生命-1 生命-1 生命

可尽情发挥创意,添加更多药水或调整效果!

每场战斗胜利后,玩家有 30% 概率发现药水,并可选择喝或不喝。不喝则药水消失。玩家饮用前不知药水类型与大小,饮用后揭晓并立即生效。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"