(理解多态的意义)编写教学游戏:认识动物

任务描述

本关任务:编写一个教学游戏,通过展示每个动物的表现(move和shout),教会小朋友认识动物。程序运行后,将在动物园(至少包含Dog Bird Frog)中挑选出10个动物,前5个用于教学,后5个用于测试。为了保证游戏的趣味性和学习、测评质量,这10个动物是从动物园所有动物中随机挑选的。 游戏运行的示意界面如下图所示,其中下划线为小朋友的回答

,

详细说明(类的设计)

基类: Animal

数据成员:

 
  1. string name; // protected成员,缺省值为类名,即Animal对象的缺省名字为Animal,Dog对象的缺少名字为Dog, 以此类推
  2. 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;
  3. int animalNum; // private成员,记录动物园中所有动物总数。

成员函数:

 
  1. Animal(string = "animal");//缺省构造函数
  2. void move(); // 空函数,供派生类继承
  3. void shout();// 空函数,供派生类继承
  4. string getName(); //返回name
  5. int getAge(); // 返回age;
  6. void setName(string s); // 将s赋值给name
  7. int getNum(); // 返回animalNum
派生类:

三个派生类Dog Frog Bird 分别 public继承 Animal,每个派生类会重新定义自己的move()shout()

  • Dog

增加数据成员 int dogNum; // private成员,记录动物园中Dog总数 定义成员函数

 
  1. Dog(string = "Dog");//缺省构造函数,name为Dog,age为当前Dog总数
  2. void move();// 输出"run X feet! " (X为5+0.1*age 末尾有个空格,不回车换行)
  3. void shout(); // 输出"wang wang, It is <名字><年龄><回车> 例:wang wang, It is Dog age 1
  4. 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 计算得到
应用程序说明:
 
  1. // 通用函数,通过调用move()和shout()展示形参所指向的某种动物的行为特征
  2. void showAnimal(Animal *pa)
  3. {
  4. pa->move();
  5. pa->shout();
  6. }

主函数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

预期输出:

 
  1. There are 30 animals in the zoo
  2. 10 dogs, 5 frogs, 15 birds
  3. Let's study!
  4. run 5.7 feet! wang wang, It is Dog age 7
  5. fly 11.1 feet! qiu qiu, It is Bird age 11
  6. fly 10.4 feet! qiu qiu, It is Bird age 4
  7. run 5.1 feet! wang wang, It is Dog age 1
  8. fly 10.1 feet! qiu qiu, It is Bird age 1
  9. Let's have a test!
  10. jump 1.1 feet! gua gua, It is Animal age 1
  11. Guess! What type of animal is It?
  12. You are right!
  13. jump 1.3 feet! gua gua, It is Animal age 3
  14. Guess! What type of animal is It?
  15. You are right!
  16. fly 10.2 feet! qiu qiu, It is Animal age 2
  17. Guess! What type of animal is It?
  18. You are right!
  19. run 5.2 feet! wang wang, It is Animal age 2
  20. Guess! What type of animal is It?
  21. You are right!
  22. run 5.5 feet! wang wang, It is Animal age 5
  23. Guess! What type of animal is It?
  24. You are right!
  25. 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),当数组在栈上被创建时,数组中的每个对象都会通过其默认构造函数进行初始化。

用于本人在大学期间的学习记录

  • 34
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值