24.x — 第 24 章总结与测验

总结

继承允许我们模拟两个对象之间的“is-a”关系。被继承的对象称为父类、基类或超类。执行继承的对象称为子类、派生类或子类。

当一个派生类从基类继承时,派生类会获得基类的所有成员。

当一个派生类被构造时,类中的基类部分会首先被构造,然后派生类部分才会被构造。更详细地说:

  1. 为派生类预留内存(足够基类和派生类两部分使用)。
  2. 调用适当的派生类构造函数。
  3. 首先使用适当的基类构造函数构造基类对象。如果未指定基类构造函数,则将使用默认构造函数。
  4. 派生类的初始化列表初始化派生类的成员。
  5. 执行派生类构造函数的主体。
  6. 控制权返回给调用者。

销毁按相反的顺序进行,从最派生类到最基类。

C++ 有 3 种访问修饰符:public、private 和 protected。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)

#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;
	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)

#include <iostream>

class Base
{
protected:
	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;
}

显示答案

e)

#include <iostream>

class Base
{
protected:
	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'; }
};

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

        // note: no print() function here
};

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

	return 0;
}

显示答案

问题 #2

a) 编写一个 Apple 类和一个 Banana 类,它们都派生自一个共同的 Fruit 类。Fruit 应该有两个成员:名称和颜色。

以下程序应该运行

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;

	std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n";
	std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n";
	std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n";
	
	return 0;
}

并产生结果

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 类。生物有 5 个属性:名称(std::string)、符号(char)、生命值(int)、每次攻击造成的伤害(int)以及携带的金币数量(int)。将这些实现为类成员。编写一组完整的 getter(每个成员一个 get 函数)。添加另外三个函数:void reduceHealth(int) 将生物的生命值减少一个整数值。bool isDead() 在生物生命值为 0 或更少时返回 true。void addGold(int) 为生物添加金币。

以下程序应该运行

#include <iostream>
#include <string>

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。Player 还有一个额外成员,玩家的等级,初始为 1。玩家有一个自定义名称(由用户输入),使用符号“@”,初始生命值为 10,初始伤害为 1,没有金币。编写一个名为 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。怪物没有非继承的成员变量。

首先,编写一个从 Creature 继承的空 Monster 类,然后在 Monster 类内部添加一个名为 Type 的枚举,其中包含此游戏中我们将拥有的 3 种怪物的枚举器:`dragon`、`orc` 和 `slime`(你还需要一个 `max_types` 枚举器,因为这稍后会派上用场)。

显示答案

d) 每种怪物类型将拥有不同的名称、符号、起始生命值、金币和伤害。以下是每种怪物类型的数据表

类型名称符号生命值伤害金币
D204100
兽人兽人o4225
史莱姆史莱姆s1110

下一步是编写一个 Monster 构造函数,以便我们可以创建怪物。Monster 构造函数应将 Type 枚举作为参数,然后创建一个具有该类型怪物相应统计数据的 Monster。

有多种不同的方法来实现这一点(有些更好,有些更差)。然而在这种情况下,因为我们所有的怪物属性都是预定义的(不是随机或按生物定制的),我们可以使用一个查找表。我们的查找表将是一个 Creature 的 C 风格数组,通过 Type 索引数组将返回该 Type 相应的 Creature。

由于此生物表是怪物特有的,我们可以在 Monster 类中将其定义为 `static inline Creature monsterData[] { }`,并用我们的 Creature 元素进行初始化。

我们的 Monster 构造函数就变得简单了:我们可以调用 Creature 复制构造函数,并传递来自 monsterData 表中相应的 Creature。

以下程序应编译通过

#include <iostream>
#include <string>

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()` 的 `static` 函数。此函数应从 `0` 到 `max_types-1` 中选择一个随机数,并返回一个具有该 `Type` 的怪物(通过值)(您需要将 `int` `static_cast` 为 `Type` 才能将其传递给 `Monster` 构造函数)。

8.15 课 -- 全局随机数 (Random.h) 包含可用于选择随机数的代码。

以下 main 函数应该运行

#include <iostream>
#include <string>

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 级(胜利)时结束
如果玩家死亡,游戏应该告诉玩家他们当前的等级和拥有的金币数量。
如果玩家获胜,游戏应该告诉玩家他们赢了,以及他们有多少金币

这是一个游戏会话示例

输入你的名字:Alex
欢迎,Alex
你遇到了一只史莱姆 (s)。
(R)逃跑或(F)战斗:f
你攻击史莱姆造成 1 点伤害。
你杀死了史莱姆。
你现在是 2 级了。
你找到了 10 枚金币。
你遇到了一条龙 (D)。
(R)逃跑或(F)战斗:r
你逃跑失败了。
巨龙对你造成了 4 点伤害。
(R)逃跑或(F)战斗:r
你成功逃脱了。
你遇到了一只兽人 (o)。
(R)逃跑或(F)战斗:f
你攻击兽人造成 2 点伤害。
兽人对你造成了 2 点伤害。
(R)逃跑或(F)战斗:f
你攻击兽人造成 2 点伤害。
你杀死了兽人。
你现在是 3 级了。
你找到了 25 枚金币。
你遇到了一条龙 (D)。
(R)逃跑或(F)战斗:r
你逃跑失败了。
巨龙对你造成了 4 点伤害。
你死在了 3 级,拥有 35 枚金币。
可惜你带不走它们!

提示:创建 4 个函数

  • main() 函数应处理游戏设置(创建 Player)和主游戏循环。
  • fightMonster() 处理玩家与单个怪物之间的战斗,包括询问玩家想要做什么,处理逃跑或战斗情况。
  • attackMonster() 处理玩家攻击怪物,包括升级。
  • attackPlayer() 处理怪物攻击玩家。

显示答案

g) 额外奖励
读者 Tom 没有把剑磨得足够锋利,无法击败强大的巨龙。通过实现以下不同尺寸的药水来帮助他

类型效果(小型)效果(中型)效果(大型)
生命值+2 生命值+2 生命值+5 生命值
力量+1 伤害+1 伤害+1 伤害
毒药-1 生命值-1 生命值-1 生命值

尽情发挥创意,添加更多药水或改变它们的效果!

玩家在每次赢得战斗后有 30% 的机会找到一瓶药水,并可以选择喝或不喝。如果玩家不喝药水,它就会消失。玩家在喝下药水之前不知道药水的类型,喝下后药水的类型和大小会揭示出来,并且效果会生效。

在以下示例中,玩家找到了一瓶毒药并因喝下它而死亡(在此示例中,毒药的伤害性更大)

You have encountered a slime (s).
(R)un or (F)ight: f
You hit the slime for 1 damage.
You killed the slime.
You are now level 2.
You found 10 gold.
You found a mythical potion! Do you want to drink it? [y/n]: y
You drank a Medium potion of Poison
You died at level 2 and with 10 gold.
Too bad you can't take it with you!

显示提示

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
655 评论
最新
最早 最多投票
内联反馈
查看所有评论