任务描述
本关任务:编写一个教学游戏,通过展示每个动物的表现(move和shout),教会小朋友认识动物。程序运行后,将在动物园(至少包含Dog Bird Frog)中挑选出10个动物,前5个用于教学,后5个用于测试。为了保证游戏的趣味性和学习、测评质量,这10个动物是从动物园所有动物中随机挑选的。 游戏运行的示意界面如下图所示,其中下划线为小朋友的回答
详细说明(类的设计)
基类: Animal
数据成员:
string name; // protected成员,缺省值为类名,即Animal对象的缺省名字为Animal,Dog对象的缺少名字为Dog, 以此类推
int age; // private成员,Animal对象的缺省值为0, 而对于Dog Frog 和Birds对象,它是第几个对象,age的缺省值为几。例如,声明的第1个dog对象,age为1, 第2个dog对象,age为2, 同理,第1个Bird对象,age为1,第5个Bird对象,age为5;
int animalNum; // private成员,记录动物园中所有动物总数。
成员函数:
Animal(string = "animal");//缺省构造函数
void move(); // 空函数,供派生类继承
void shout();// 空函数,供派生类继承
string getName(); //返回name
int getAge(); // 返回age;
void setName(string s); // 将s赋值给name
int getNum(); // 返回animalNum
派生类:
三个派生类Dog
Frog
Bird
分别 public
继承 Animal
,每个派生类会重新定义自己的move()
和shout()
Dog
类
增加数据成员 int dogNum; // private成员,记录动物园中Dog总数
定义成员函数
Dog(string = "Dog");//缺省构造函数,name为Dog,age为当前Dog总数
void move();// 输出"run X feet! " (X为5+0.1*age 末尾有个空格,不回车换行)
void shout(); // 输出"wang wang, It is <名字><年龄><回车> 例:wang wang, It is Dog age 1
int getNum(); //输出当前的Dog总数
- Frog类 与Dog类类似,增加数据成员
frogNum
记录frog
总数,并重新定义相关的成员函数 例如:声明的第3个Frog对象调用move和shout,将输出jump 1.3 feet! gua gau, It is Frog age 3
其中 1.3 由1+0.1*age 计算得到 - Bird类 与Dog类类似,增加数据成员
birdNum
记录bird
总数,并重新定义相关的成员函数 例如:声明的第4个Bird对象调用move和shout,将输出fly 10.4 feet! qiu qiu, It is Bird age 4
其中 10.4 由10+0.1*age 计算得到
应用程序说明:
// 通用函数,通过调用move()和shout()展示形参所指向的某种动物的行为特征
void showAnimal(Animal *pa)
{
pa->move();
pa->shout();
}
主函数main 假设动物园中有有10只dog,5只frog,15只bird (此外做了简化处理,更完善的程序应该允许用户自定义或按配置生成) 从中随机挑选10只动物, 将其地址放入animalList指针数组 依次展示前5只动物(调用showAnimal()
)供小朋友学习 为了测试,将后5只动画的名字更改为Animal 然后依次展示后5只动物(调用showAnimal()
),让小朋友根据动物表现回答是什么,答对了加20,答错不得分 最后输出得分
任务分析
动物园中有很多动物,以后还会有新动物添加进来。用于学习和测试的可能是任何一种动物,当前任务的要求是随机产生,也许以后还有可能增加教师任选功能。这管怎么,在编程实现时,不可能知道将来需要展示的是哪一种动物。因此,需要编写一个通用的show函数,通过多态实现动态联编,也就是运行时确定展示的是哪种动物的move和shout。
相关知识
1 虚函数与多态
- 多态:指操作接口具有表现多种形态的能力。
- 实现多态:将基类与派生类中的原型相同的函数声明为虚函数,则当通过基类指针调用虚函数时,由指针指向的对象的类型决定调用的是哪个类的函数,即该基类指针调用虚函数,具有多种执行状态(多态)
- 说明:若一个成员函数在继承树中基类和派生类多次定义,当基类指针指向派生类对象时,由指针的类型决定调用哪个成员函数(编译时就确定了调用谁)。但如果声明成员函数为虚函数,即加了virtual关键字,则用基类指针调用该成员函数时,由指向对象的类型决定调用哪个类的成员函数
- 虚函数语法:
virtual 函数类型 函数名(形参表)
2 纯虚函数与抽象类
- 纯虚函数语法:virtual 返回值类型 函数名 (函数参数) = 0;
- 纯虚函数不需要实现,供派生类重新定义
- 只要类中包含一个或多个纯虚函数,即为抽象类
- 抽象类不是完整的类,无法实例化,仅用于做基类,供派生类继承
- 如果派生类中没有重新定义纯虚函数,则编译报错
3 如何获取对象的类型
typeid运算符
:typeid()
是一个运算符,类似于sizeof()
运算符。其运算结果为类型信息,是一个typeinfo
对象,typeinfo
的成员函数name()
返回类型名字。- 例如
Dog d;
则typeid(d).name()
的值为 Dog, 不同的编译器执行结果略不有同,但一定包含类型名。因此,代码中用了strstr函数来查看执行结果中是否包含指定的名字
4 所使用的头文件说明
- 使用
typeid
需要包含头文件<typeinfo>
strstr
函数则是标准C字符串函数,需要包含头文件<cstring>
- 类Animal成员name为
string
对象,需要包含头文件<string>
- 调用
rand()
函数,需要包含头文件<stdlib>
编程要求
根据提示,在右侧编辑器补充完善代码,实现上述功能。 注意:已有代码已实现了部分成员函数,你可以补充完善,允许在已有代码上添加新的代码和标识(如virtual static等修饰符),但不允许删除或替换已有代码。
测试说明
平台会对你编写的代码进行测试。注意,由于Linux和Window环境下随机数范围不同,因此在本平台的运行结果(随机挑选的动物)可能与你自己电脑中Codeblock等IDE运行结果有所不同。
测试输入:6行, 第一行为随机数种子,后5行是小朋友猜动物的回答 3
Frog Frog Bird Dog Dog
预期输出:
There are 30 animals in the zoo
10 dogs, 5 frogs, 15 birds
Let's study!
run 5.7 feet! wang wang, It is Dog age 7
fly 11.1 feet! qiu qiu, It is Bird age 11
fly 10.4 feet! qiu qiu, It is Bird age 4
run 5.1 feet! wang wang, It is Dog age 1
fly 10.1 feet! qiu qiu, It is Bird age 1
Let's have a test!
jump 1.1 feet! gua gua, It is Animal age 1
Guess! What type of animal is It?
You are right!
jump 1.3 feet! gua gua, It is Animal age 3
Guess! What type of animal is It?
You are right!
fly 10.2 feet! qiu qiu, It is Animal age 2
Guess! What type of animal is It?
You are right!
run 5.2 feet! wang wang, It is Animal age 2
Guess! What type of animal is It?
You are right!
run 5.5 feet! wang wang, It is Animal age 5
Guess! What type of animal is It?
You are right!
Your score: 100
开始你的任务吧,祝你成功!
答案
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <typeinfo>
using namespace std;
/********Begin*****/
// 允许在Begin -End之间任何位置添加代码,允许在现有代码(包括类的定义)中添加内容,但不允许删除或替换现有代码
// 基类 Animal
class Animal
{
public:
Animal(string name = "Animal", int age = 0);//缺省构造函数,默认名字为Animal,年龄为0
virtual void move()=0 ;//纯虚函数 空操作,仅供继承
virtual void shout()=0 ;//纯虚函数 空操作,仅供继承
string getName() { return name; } //返回name
void setName(string s) { name = s; } // 给name赋值
int getAge() { return age; } // 返回age
static int getNum() { return animalNum; }// 返回各种类型动物总数
protected:
string name; //名字
private:
int age;//年龄
static int animalNum;//记录各种类型动物对象的总数
};
//派生类Dog
class Dog : public Animal
{
public:
Dog(string = "Dog");// 缺省构造函数,默认名字为Dog,年龄为当前Dog对象的总数(含自己)
void move(); // 输出 "run X feet! " 其中X为 5+0.1*age
void shout();// 输出"wang wang, It is XX age #" 其中XX为自己的name, #为自己的age
static int getNum() { return dogNum; } //返回Dog总数
private:
static int dogNum;//记录Dog对象的总数
};
//派生类Frog
class Frog : public Animal
{
public:
Frog(string name = "Frog");// 缺省构造函数,默认名字为Frog,年龄为当前Frog对象的总数(含自己)
void move(); // 输出 "jump X feet! " 其中X为 1+0.1*age
void shout();// 输出"gua gau, It is XX age #" 其中XX为自己的name, #为自己的age
static int getNum() { return frogNum; }
private:
static int frogNum;//记录Frog对象的总数
};
//派生类Bird
class Bird : public Animal
{
public:
Bird(string name = "Bird");// 缺省构造函数,默认名字为Bird,年龄为当前Bird对象的总数(含自己)
void move(); // 输出 "fly X feet! " 其中X为 10+0.1*age
void shout();// 输出"qiu qiu, It is XX age #" 其中XX为自己的name, #为自己的age
static int getNum() { return birdNum; }//返回Bird对象的总数
private:
static int birdNum;//记录Bird对象的总数
};
// 你可以在此处补充必要的代码,以实现上述类中定义的功能
//Animal类构造函数
Animal::Animal(string name, int age)
{
this->name = name;
this->age = age;
animalNum++;
}
//类外初始化静态数据
int Animal::animalNum = 0;
int Dog::dogNum = 0;
int Frog::frogNum = 0;
int Bird::birdNum = 0;
//Dog类
Dog::Dog(string name) :Animal(name, dogNum + 1) //构造函数,使用初始化列表来调用基类构造函数,并递增 dogNum (注意递增的顺序)
{
dogNum++;
}
void Dog::move()
{
cout << "run " << 5+0.1*getAge() << " feet! ";
}
void Dog::shout()
{
cout << "wang wang, It is "<<name <<" age " << this->getAge()<<endl;
}
//Frog类
Frog::Frog(string name) :Animal(name, frogNum + 1) //构造函数,使用初始化列表来调用基类构造函数,并递增 FrogNum (注意递增的顺序)
{
frogNum++;
}
void Frog::move()
{
cout << "jump " << 1+0.1*getAge() << " feet! ";
}
void Frog::shout()
{
cout << "gua gua, It is " << name << " age " << this->getAge() << endl;
}
//Bird类
Bird::Bird(string name) :Animal(name, birdNum + 1) //构造函数,使用初始化列表来调用基类构造函数,并递增 birdNum (注意递增的顺序)
{
birdNum++;
}
void Bird::move()
{
cout << "fly " << 10+0.1*getAge()<< " feet! ";
}
void Bird::shout()
{
cout << "qiu qiu, It is " << name << " age " << this->getAge() << endl;
}
/*******End**********/
// 以下代码不需要做任何改动,但你可以阅读并学习如何实现应用程序的功能,包括如何编写通用的showAnimal函数
void showAnimal(Animal* pa)
{
pa->move();
pa->shout();
}
int main()
{
// 动物园中有10只dog,5只frog,15只bird 对于类类型的数组,当数组在栈上被创建时,数组中的每个对象都会通过其默认构造函数进行初始化
Dog dogList[10];
Frog frogList[5];
Bird birdList[15];
Animal* animalList[10]; // animal类型的指针数组,用于存放随机挑选出的动物的地址
int seeds;
cin >> seeds;
srand(seeds); // 随机数种子
int totalnum = Animal::getNum();
cout << "There are " << totalnum << " animals in the zoo" << endl;
cout << Dog::getNum() << " dogs, " << Frog::getNum() << " frogs, " << Bird::getNum() << " birds\n\n";
// 随机挑选10只动物, 将其地址放入animalList指针数组
for (int i = 0; i < 10; i++)
{
int n = rand() % Animal::getNum(); // 随机生成动物ID
if (n < Dog::getNum())
animalList[i] = dogList + n;
else if (n < Dog::getNum() + Frog::getNum())
animalList[i] = frogList + (n - Dog::getNum());
else
animalList[i] = birdList + (n - Dog::getNum() - Frog::getNum());
}
cout << "Let's study!" << endl;
// 用前5只动物让小朋友学习
for (int i = 0; i < 5; i++)
{
showAnimal(animalList[i]);
}
// 用后5只动物来测试,为了达到测试效果,先将他们的名字统一改为Animal, 再开始测试
for (int i = 5; i < 10; i++)
{
animalList[i]->setName("Animal");
}
cout << "\nLet's have a test!" << endl << endl;
int score = 0;
// 开始测试,依次显示后5只动物,让小朋友猜
for (int i = 5; i < 10; i++)
{
showAnimal(animalList[i]); // 显示
cout << "Guess! What type of animal is It?" << endl;
char ns[10];
cin >> ns; // 让小朋友回答动物类型 Dog Frog 或者Bird
if (strstr(typeid(*animalList[i]).name(), ns)) // 判断输入的动物类型是否出现在当前指针指向对象的类型名字中
{
cout << "You are right!\n" << endl;
score += 20;
}
else
cout << "You are wrong!\n" << endl;
}
cout << "Your score: " << score << endl;
return 0;
}
总结
1.最初编写代码时考虑到基类void move ()和void shout ()可以写为虚函数,结果出现了错误 “LNK200无法解析的外部符号 "public: virtual void,cdeclAnimal:move(void)"” 原因是源码的14 15 行中声明虚函数时,未定义(按照头歌的写法),需要修改为 声明时定义,即在“()”后添加 “=0”(纯虚函数的定义),或者添加“{}”(空虚函数)。
2.需要将各类中的动物总数的数据类型写为静态类型(static),在类外初始化。从编程思路方面考虑,因为用了类类型数组,为了让每一个成员数据得到传递,保证数据的准确;从代码逻辑方面考虑,如果不用static类型,编译会出现错误“调用非静态成员函数需要一个对象”因为在头歌的代码中,主函数在调用类成员用的是“Animal::getNum()”这种格式,说明在前面类定义时,类成员中的(动物)对象的总数应该为static类型(判断原因如下图)。
3.在Dog
类的构造函数中,我们使用了初始化列表来调用基类Animal
的构造函数,并传递了适当的参数。我们还递增了dogNum
,但是要注意递增的顺序,因为dogNum
应该在传递给基类之前被递增,以反映新创建的对象的年龄。
补充: 将virtual void move() = 0;
和virtual void shout() = 0;
纯虚函数的好处是纯虚函数确保派生类必须实现特定的接口,使得基类成为抽象类,不能被实例化。这有助于定义统一的接口,增强代码的一致性和可维护性,同时支持多态性。因为每一个类对move和shout都有具体的不同的实现。
对于类类型的数组(如Dog、Frog和Bird),当数组在栈上被创建时,数组中的每个对象都会通过其默认构造函数进行初始化。
用于本人在大学期间的学习记录