【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
目录😋
三、类Animal成员name为string对象,需要包含头文件
任务描述
本关任务:编写一个教学游戏,通过展示每个动物的表现(move和shout),教会小朋友认识动物。程序运行后,将在动物园(至少包含Dog Bird Frog)中挑选出10个动物,前5个用于教学,后5个用于测试。为了保证游戏的趣味性和学习、测评质量,这10个动物是从动物园所有动物中随机挑选的。
游戏运行的示意界面如下图所示,其中下划线为小朋友的回答
详细说明(类的设计)
基类: Animal
数据成员:
成员函数:
派生类:
三个派生类Dog Frog Bird 分别 public继承 Animal,每个派生类会重新定义自己的move()和shout()
- 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()展示形参所指向的某种动物的行为特征:
主函数main 设计:
假设动物园中有有10只dog,5只frog,15只bird (此处做简化处理,更完善的程序应该允许用户自定义或按配置生成)
从中随机挑选10只动物, 将其地址放入animalList指针数组。依次展示前5只动物(调用showAnimal())供小朋友学习。为了测试,将后5只动画的名字更改为Animal,然后依次展示后5只动物(调用showAnimal()),让小朋友根据动物表现回答是什么,答对了加20,答错不得分,最后输出得分。
相关知识
为了完成本关任务,你需要掌握:
- 虚函数与多态
- 纯虚函数与抽象类
- 如何获取对象的类型
- 所使用的头文件说明
1. 虚函数与多态
一、多态的概念与意义
多态是面向对象编程中的一个重要特性,它使得操作接口能够呈现出多种形态。从实际应用角度来看,多态允许不同类型的对象对同一消息(函数调用)做出不同的响应,这极大地增强了程序设计的灵活性和可扩展性。
例如,在一个图形绘制系统中,可能有多种图形类,如圆形(Circle)、矩形(Rectangle)、三角形(Triangle)等,它们都派生自一个基类图形(Shape)。而绘制这个操作对于不同的图形具体实现是不一样的,多态就能让我们使用统一的 “绘制” 接口(比如在基类中定义一个名为 draw 的虚函数),当针对不同具体图形对象调用这个接口时,能够自动执行对应图形各自正确的绘制逻辑,这样的设计使得代码的结构更加清晰、易于维护和扩展,后续如果新增一种图形类型,只需要按照统一的接口规范去实现其对应的绘制函数即可。
二、虚函数实现多态的原理
在 C++ 等支持面向对象编程且有多态特性的语言中,将基类与派生类中原型相同(函数返回类型、函数名、参数列表都相同)的函数声明为虚函数,是实现多态的关键手段。
当通过基类指针(或者引用)去调用虚函数时,程序运行时会根据指针(或引用)实际指向(或绑定)的对象的类型来决定调用的是哪个类的函数。也就是说,在编译阶段并不能确定具体调用哪个类中的函数实现,而是推迟到了运行阶段去动态判断,这也是所谓的动态绑定机制。
对比来看,如果一个成员函数在继承树中基类和派生类多次定义,但没有声明为虚函数,那么当基类指针指向派生类对象时,调用哪个成员函数是由指针的类型决定的,而且这种调用关系在编译时就已经确定了,是一种静态绑定机制。例如:
但如果将基类中的
func
函数声明为虚函数(virtual void func() {... }
),同样的代码逻辑下,ptr->func();
就会输出"Derived::func called"
,因为此时是动态绑定,根据指针实际指向的Derived
类对象来决定调用Derived
类中的func
函数实现了多态的效果。
三、虚函数的语法细节
虚函数的语法形式为
virtual 函数类型 函数名(形参表)
,具体如下:
- virtual 关键字:它必须放在函数声明的最前面,用于告知编译器这个函数是虚函数,要采用动态绑定机制。比如在基类的类定义中:
这里的
makeSound
函数就是虚函数,意味着后续如果有派生类重写这个函数,通过基类指针(或引用)调用该函数时会根据实际对象类型决定调用哪个类中的版本。- 函数类型、函数名和形参表:
- 函数类型:规定了函数返回值的类型,可以是基本数据类型(如
int
、double
等),也可以是指针、引用或者自定义的类类型等。例如:virtual int getValue();
表示返回值类型为int
的虚函数。- 函数名:遵循标识符的命名规则,要能够清晰体现函数的功能,方便代码阅读和理解,同一类中函数名不能重复(重载函数除外,重载是根据参数列表不同来区分的)。
- 形参表:列出函数接受的参数的类型和参数名(参数名可省略,如果只是声明函数时,在函数实现时再写具体参数名),形参表决定了函数的参数特征,在派生类重写虚函数时,形参表必须和基类中对应的虚函数形参表完全一致(包括参数类型、个数、顺序等),否则就不是重写(而是函数重载了),无法实现多态效果。
派生类中重写基类的虚函数时,除了形参表要一致外,还有一些其他注意事项:
- 函数的
const
修饰符也要保持一致,如果基类虚函数是const
成员函数(如virtual void display() const {... }
),那么派生类重写的函数也得是const
成员函数。- 函数的返回类型在满足一定条件下可以有协变类型(即返回类型可以不同,但要满足是指针或者引用类型,并且派生类返回的指针(或引用)指向(或绑定)的类型是基类返回的指针(或引用)指向(或绑定)类型的派生类),不过一般简单的情况还是返回类型相同居多。
正确使用虚函数的语法并遵循重写的规则,才能有效地利用多态机制来设计出灵活且易于维护的面向对象程序。
2. 纯虚函数与抽象类
一、纯虚函数的本质与语法规则
纯虚函数是一种特殊的虚函数,其语法形式为
virtual 返回值类型 函数名 (函数参数) = 0;
。这种独特的语法结构表明该函数没有具体的函数体实现,它主要是为派生类提供一个统一的接口规范,起到一种 “占位” 和 “约束” 的作用。例如,在一个图形处理库中,定义一个基类Shape
,其中有一个纯虚函数draw
,表示绘制图形的操作,但由于不同形状的图形绘制方式各异,在基类中无法给出通用的实现,于是声明为纯虚函数:二、抽象类的判定与特性
当一个类包含一个或多个纯虚函数时,这个类就成为了抽象类。抽象类的关键特性在于它不是一个完整的、可直接实例化的类,它更像是一个蓝图或者框架,专门用于被其他派生类继承并扩展。以
Shape
类为例,因为它含有纯虚函数draw
,所以它是抽象类,不能像普通类那样创建对象,如Shape s;
这样的代码是错误的,会在编译阶段被禁止。
三、抽象类在继承体系中的角色与意义
抽象类作为基类,在面向对象的继承体系中具有极其重要的地位。它为派生类建立了一套通用的接口标准,强制派生类必须对这些纯虚函数进行重新定义,以实现各自特定的行为。比如,从
Shape
类派生Circle
类和Rectangle
类时,Circle
类和Rectangle
类都需要重新定义draw
函数来实现圆形和矩形的具体绘制逻辑:如果派生类没有重新定义纯虚函数,那么该派生类依然是抽象类,不能被实例化,并且在编译时会报错。这一机制确保了在继承体系中,从抽象基类派生的具体类都具备符合预期的行为和功能,保证了代码的规范性和一致性,使得整个程序结构更加清晰、易于维护和扩展。例如,如果有一个
Triangle
类派生自Shape
类,但忘记定义draw
函数:在尝试实例化
Triangle
类对象时,如Triangle t;
,编译过程就会报错,提示Triangle
类是抽象类,因为它没有实现从基类继承的纯虚函数draw
。
四、抽象类的应用场景与优势
在大型软件项目中,抽象类被广泛应用。例如,在游戏开发中,各种游戏角色可能都继承自一个抽象的
Character
类,该类定义了纯虚函数move
、attack
等,不同类型的角色(如战士、法师、刺客等)在继承Character
类后,必须根据自身特点实现这些纯虚函数,从而实现各自独特的移动和攻击方式。这种设计模式使得游戏角色的管理和扩展变得极为方便,新的角色类型只需遵循Character
类设定的接口规范进行开发即可轻松融入游戏体系。又如,在图形用户界面(GUI)编程中,各种窗口组件(如按钮、文本框、列表框等)可能都派生自一个抽象的
Widget
类,该类包含纯虚函数draw
、handleEvent
等。不同的组件通过重写这些纯虚函数来实现自身的绘制和事件处理逻辑,这样的设计使得 GUI 框架具有高度的可扩展性和灵活性,能够方便地添加新的组件类型以满足不断变化的用户需求。
3. 如何获取对象的类型
一、typeid 运算符概述
在 C++ 编程语言中,typeid 运算符扮演着十分重要的角色,它与 sizeof () 运算符类似,都是语言内置的、能够在编译或运行阶段提供特定信息的机制。typeid 运算符的核心作用在于获取对象或者类型的类型信息,其运算结果会返回一个 typeinfo 类型的对象。这个 typeinfo 类是 C++ 标准库中定义好的一个类,它封装了与类型相关的诸多细节信息,而其中比较常用的就是成员函数 name (),通过调用这个函数能够获取到对应的类型名字,进而帮助我们知晓具体的对象类型。
二、使用示例与不同编译器表现差异
例如,我们定义一个简单的类
Dog
,并且创建一个该类的对象d
,像下面这样:按照常规理解,我们期望
typeid(d).name()
的值为"Dog"
,但实际情况是不同的编译器在执行这一操作时,其返回结果会略有不同。这是因为 C++ 标准并没有严格规定typeinfo::name()
函数返回的具体字符串格式,只是要求其返回结果一定包含类型名相关的信息。所以,不同的编译器厂商会根据自身的实现方式来确定具体的返回内容。比如,在某些编译器(如 GNU C++ 编译器)下,返回的字符串可能不仅仅是简单的类名,可能还会附带一些其他的标识信息,像是
N3DogE
这样的形式(这里只是示例,实际格式因版本等因素可能有变化),其中的Dog
就是我们定义的类名,而前面和后面的字符则是编译器添加的额外标识。而在另一些编译器(如 Visual C++ 编译器)中,返回的格式又可能是其他样子。
三、借助 strstr 函数查看执行结果
鉴于不同编译器返回的typeinfo::name()
结果存在差异,在实际代码编写中,如果我们想要确切地知道某个对象的类型是否符合预期,常常会使用标准库中的strstr
函数来辅助查看执行结果中是否包含指定的名字。strstr
函数的功能是在一个字符串中查找另一个子字符串首次出现的位置,如果找到了,就返回指向该子字符串在原字符串中起始位置的指针;如果没找到,则返回空指针。
以下是一个简单的示例代码,用于演示如何结合
strstr
函数来判断对象类型是否符合期望:在上述代码中,我们分别创建了
Dog
类和Cat
类的对象,然后获取它们对应的类型名字符串,再通过strstr
函数去检查字符串中是否包含相应的类名关键字,以此来判断对象的类型是否符合我们的预期。这种方式在处理一些复杂的、涉及多态和继承场景下的类型判断时尤为有用。例如,在存在继承关系的类层次结构中,通过基类指针指向派生类对象时,我们可以利用
typeid
结合strstr
来准确判断指针实际指向的对象具体属于哪一个派生类,进而执行相应的、针对该派生类特性的操作逻辑,确保程序行为的正确性和灵活性。虽然
typeid
运算符在获取对象类型信息方面提供了便利,但由于编译器实现的差异,我们需要采用一些合适的辅助手段(如strstr
函数)来更精准地处理和利用其返回结果,以满足实际编程中的类型判断需求。
4. 头文件说明
一、使用typeid需要包含头文件<typeinfo>
在 C++ 编程中,当我们想要使用
typeid
运算符来获取对象或者类型的相关类型信息时,必须要包含<typeinfo>
头文件。这个头文件中定义了type_info
类以及和类型信息操作相关的各种声明与定义。
type_info
类作为核心,其对象就是typeid
运算符运算后返回的结果类型。例如,当我们写下像typeid(someObject).name()
这样的代码去获取某个对象someObject
的类型名称时,编译器需要知道typeid
这个运算符如何工作以及type_info
类中name
函数等成员的具体实现细节,而这一切的基础就是引入了<typeinfo>
头文件。如果在代码中没有包含这个头文件就使用
typeid
运算符,编译器会报错提示找不到相应的符号或者类型定义,导致编译无法顺利进行。而且,<typeinfo>
头文件不仅仅服务于简单的类型判断场景,在一些复杂的多态应用、模板编程中涉及到类型比较或者类型特征判断时,也发挥着不可或缺的作用,确保程序能够准确地知晓并处理不同对象所对应的类型情况。
二、strstr函数则是标准C字符串函数,需要包含头文件<cstring>
strstr
函数是标准 C 语言中用于处理字符串的函数,在 C++ 编程里同样可以使用,不过需要包含<cstring>
头文件。这个头文件中包含了诸多和字符串操作相关的函数声明,像字符串复制函数strcpy
、字符串拼接函数strcat
、字符串比较函数strcmp
等等,strstr
只是其中之一。其中,
strstr
函数的功能是在一个给定的字符串(通常称为主字符串)中查找另一个指定的字符串(通常称为子字符串)首次出现的位置。其函数原型大致如下:haystack
表示主字符串,needle
表示要查找的子字符串。函数返回值是一个指向主字符串中首次出现子字符串位置的指针,如果在主字符串中没有找到对应的子字符串,那么返回空指针nullptr
(在 C 语言中返回NULL
)。当我们在代码中需要利用
strstr
函数去判断某个字符串中是否包含特定的内容,比如前面提到的结合typeid
获取的类型名字符串来判断是否包含期望的类名等情况时,就一定要先包含<cstring>
头文件,否则编译器无法识别strstr
函数,会出现编译错误,提示该函数未定义之类的问题,进而影响整个程序逻辑的实现。
三、类Animal成员name为string对象,需要包含头文件<string>
在 C++ 中,如果类
Animal
的成员变量name
被定义为string
对象,那就必须要包含<string>
头文件。string
类型是 C++ 标准库中提供的一个非常方便且功能强大的字符串类型,相较于传统的 C 语言风格的字符数组表示字符串(如char str[]
),它具有很多优势,比如自动管理内存、提供丰富的字符串操作方法等。
<string>
头文件中定义了std::string
类以及与之相关的众多成员函数和操作符重载等内容,使得我们可以像操作基本数据类型一样便捷地对字符串进行赋值、拼接、比较、获取长度等操作。例如:
要是没有包含
<string>
头文件,编译器在处理涉及std::string
类型的代码时,就不知道std::string
是什么,无法识别相关的构造函数、成员函数等操作,从而导致编译失败,所以这个头文件对于处理string
类型的成员变量是必不可少的。
四、调用rand()函数,需要包含头文件<stdlib>当在代码中调用
rand()
函数时,需要包含<stdlib>
头文件。rand()
函数是 C/C++ 标准库中用于生成伪随机数的一个常用函数,它会返回一个范围在0
到RAND_MAX
(RAND_MAX
是一个在<stdlib>
头文件中定义的宏常量,通常其值较大,不同系统下可能有所不同,常见值为32767
)之间的整数。例如,我们可以利用
rand()
函数来模拟一些随机事件或者为程序中的某些变量赋予随机初始值等情况,以下是一个简单示例:在上述代码中,先是通过
srand
函数结合time
函数(time
函数所在的<ctime>
头文件常配合rand
使用,用于根据当前时间设置随机数种子,使得每次运行程序生成的随机数序列不同)来初始化随机数生成器,然后调用rand()
函数获取一个随机数并输出。
如果没有包含<stdlib>
头文件就去调用rand()
函数,编译器会提示找不到该函数的定义,导致代码无法正确编译和运行,所以在涉及到需要生成随机数的编程场景中,记得要引入这个头文件来保证程序的正常运作。
编程要求
根据提示,在右侧编辑器补充完善代码,实现上述功能。
注意:已有代码已实现了部分成员函数,你可以补充完善,允许在已有代码上添加新的代码和标识(如virtual static等修饰符),但不允许删除或替换已有代码。
测试说明
平台会对你编写的代码进行测试。
注意,由于Linux和Window环境下随机数范围不同,因此在本平台的运行结果(随机挑选的动物)可能与你自己电脑中Codeblock等IDE运行结果有所不同。
测试输入:(6行, 第一行为随机数种子,后5行是小朋友猜动物的回答)
预期输出:
通关代码
测试结果
- 点赞
- 收藏
- 关注作者
评论(0)