小结
继承让我们能够在两个对象之间建立“is-a”(是一个)的关系。被继承的对象称为父类(parent class)、基类(base class)或超类(superclass);执行继承的对象称为子类(child class)、派生类(derived class)或子类(subclass)。
当派生类继承自基类时,派生类会获得基类的全部成员。
派生类对象的构造顺序如下(更详细地说):
- 为派生类分配内存(需容纳基类部分与派生类部分)。
- 调用合适的派生类构造函数。
- 首先使用合适的基类构造函数构造基类对象。若未显式指定基类构造函数,则使用默认构造函数。
- 派生类构造函数的初始化列表初始化派生类自己的成员。
- 执行派生类构造函数体。
- 将控制权返回给调用者。
析构顺序与之相反:从最派生到最基类依次析构。
C++ 有三种访问说明符:public、private、protected。protected 允许所属类本身、友元以及派生类访问该成员,但禁止其他外部访问。
类可以以 public、private 或 protected 方式继承另一个类;几乎总是采用 public 继承。
下表列出了基类成员的访问说明符与各种继承方式组合后的结果:
基类中的访问说明符 | public 继承后的访问说明符 | private 继承后的访问说明符 | protected 继承后的访问说明符 |
---|---|---|---|
Public | Public | Private | Protected |
Private | 不可访问 | 不可访问 | 不可访问 |
Protected | Protected | Private | Protected |
派生类可以:
- 增加新函数;
- 改变基类已有函数的行为;
- 改变继承成员的访问级别;
- 隐藏功能。
多重继承允许一个派生类从多个父类继承成员。除非替代方案会导致更复杂的代码,否则应避免多重继承。
测验时间
问题 #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 类型拥有不同的名称、符号、初始生命、伤害与黄金。下表给出各类型属性:
类型 | 名称 | 符号 | 生命 | 伤害 | 黄金 |
---|---|---|---|---|---|
dragon | dragon | D | 20 | 4 | 100 |
orc | orc | o | 4 | 2 | 25 |
slime | slime | s | 1 | 1 | 10 |
下一步编写 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)。
- 若怪物未死亡,怪物反击,玩家生命减少怪物伤害值。
- 游戏结束条件:玩家死亡(失败)或达到 20 级(胜利)。
- 玩家死亡时,告知其等级与黄金数量;胜利时告知胜利与黄金数量。
示例游戏流程(略,见原文)。
提示:创建 4 个函数:
- main():负责游戏初始化(创建 Player)与主循环。
- fightMonster():处理玩家与单只 Monster 的战斗,询问玩家行动,处理逃跑或战斗。
- attackMonster():处理玩家攻击怪物及升级。
- attackPlayer():处理怪物攻击玩家。
g) 附加题: 读者 Tom 的剑不够锋利,无法击败巨龙。请帮他实现以下不同尺寸的药水:
类型 | 小型效果 | 中型效果 | 大型效果 |
---|---|---|---|
Health | +2 生命 | +2 生命 | +5 生命 |
Strength | +1 伤害 | +1 伤害 | +1 伤害 |
Poison | -1 生命 | -1 生命 | -1 生命 |
可尽情发挥创意,添加更多药水或调整效果!
每场战斗胜利后,玩家有 30% 概率发现药水,并可选择喝或不喝。不喝则药水消失。玩家饮用前不知药水类型与大小,饮用后揭晓并立即生效。