第5章 多态与虚函数

学习目标

★ 了解多态的概念与实现条件

★ 掌握虚函数的定义

★ 掌握虚函数实现多态的机制

★ 掌握虚析构函数的定义与调用

★ 掌握纯虚函数与抽象类的应用

通过前面的学习,读者已经掌握了面向对象中的封装和继承的特征。本章将针对面向对象三大特征的最后一个特征——多态进行详细讲解。

5.1 多态概述

C++中的多态分为静态多态和动态多态。静态多态是函数重载,在编译阶段就能确定调用哪个函数。动态多态是由继承产生的,指同一个属性或行为在基类及其各派生类中具有不同的语义,不同的对象根据所接收的消息做出不同的响应,这种现象称为动态多态。例如,动物都能发出叫声,但不同的动物叫声不同,猫会“喵喵”、狗会“汪汪”,这就是多态的体现。面向对象程序设计中所说的多态通常指的是动态多态。

在C++中,“消息”就是对类的成员函数的调用,不同的行为代表函数的不同实现方式,因此,多态的本质是函数的多种实现形态。

多态的实现需要满足3个条件。

(1)基类声明虚函数。

(2)派生类重写基类的虚函数。

(3)将基类指针指向派生类对象,通过基类指针访问虚函数。

5.2 虚函数实现多态

如果基类与派生类中有同名成员函数,根据类型兼容规则,当使用基类指针或基类引用操作派生类对象时,只能调用基类的同名函数。如果想要使用基类指针或基类引用调用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。本节将针对虚函数实现多态进行详细讲解。

5.2.1 虚函数

虚函数的声明方式是在成员函数的返回值类型前添加virtual关键字,格式如下所示:

class 类名 
{ 
权限控制符: 
 virtual 函数返回值类型 函数名(参数列表); 
 … //其他成员 
};

声明虚函数时,有以下3点需要注意。

(1)构造函数不能声明为虚函数,但析构函数可以声明为虚函数。

(2)虚函数不能是静态成员函数。

(3)友元函数不能声明为虚函数,但虚函数可以作为另一个类的友元函数。

虚函数只能是类的成员函数,不能将类外的普通函数声明为虚函数,即virtual关键字只能修饰类中的成员函数,不能修饰类外的普通函数。因为虚函数的作用是让派生类对虚函数重新定义,它只能存在于类的继承层次结构中。

若类中声明了虚函数,并且派生类重新定义了虚函数,当使用基类指针或基类引用操作派生类对象调用函数时,系统会自动调用派生类中的虚函数代替基类虚函数。

下面通过案例演示虚函数实现多态的机制,如例5-1所示。

例5-1 polymorphic.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3 class Animal     //动物类Animal 
 4  { 
 5  public: 
 6       virtual void speak();  //声明虚函数speak() 
 7  }; 
 8 void Animal::speak()   //类外实现虚函数 
 9  { 
 10      cout<<"动物叫声"<<endl; 
 11 } 
 12 class Cat:public Animal   //猫类Cat,公有继承Animal类 
 13 { 
 14 public: 
 15      virtual void speak();  //声明虚函数speak() 
 16 }; 
 17 void Cat::speak()    //类外实现虚函数 
 18 { 
 19      cout<<"猫的叫声:喵喵"<<endl; 
 20 } 
 21 class Dog:public Animal   //狗类Dog,公有继承Animal类 
 22 { 
 23 public: 
 24      virtual void speak();  //声明虚函数speak() 
 25 }; 
 26 void Dog::speak()    //类外实现虚函数 
 27 { 
 28      cout<<"狗的叫声:汪汪"<<endl; 
 29 } 
 30 int main() 
 31 { 
 32      Cat cat;      //创建Cat类对象cat 
 33      Animal *pA=&cat;   //定义Animal类指针pA指向对象cat 
 34      pA->speak();     //通过pA调用speak()函数 
 35      Dog dog;      //创建Dog类对象dog 
 36      Animal *pB=&dog;   //定义Animal类指针pB指向对象dog 
 37      pB->speak();     //通过pB调用speak()函数 
 38     return 0; 
 39 }

例5-1运行结果如图5-1所示。

在例5-1中,第3~7行代码定义了动物类Anim al,该类声明了虚函数speak()。第8~11行代码在类外实现虚函数speak()。需要注意的是,在类外实现虚函数时,返回值类型前不能添加virtual关键字。

图5-1 例5-1运行结果

第12~16行代码定义了猫类Cat,公有继承Anim al。Cat类也声明了虚函数speak()。第21~25行代码定义了狗类Dog,公有继承Anim al类,Dog类也声明了虚函数speak()。

第32~34行代码,在m ain()函数中创建Cat类对象cat,定义Anim al类指针pA指向对象cat,然后通过pA调用speak()函数。第35~37行代码创建Dog类对象dog,定义Anim al类指针pB指向对象dog,然后通过pB调用speak()函数。

由图5-1可知,pA指针调用的是Cat类的speak()函数,输出了猫的叫声;pB调用的是Dog类的speak()函数,输出了狗的叫声。基类指针调用的永远都是派生类重写的虚函数,不同的派生类对象都有自己的表现形态。

需要注意的是,派生类对基类虚函数重写时,必须与基类中虚函数的原型完全一致,派生类中重写的虚函数前是否添加virtual,均被视为虚函数。

多学一招:override和final(C++11新标准)

override和final关键字是C++11新标准提供的两个关键字,在类的继承中有着广泛应用,下面对这两个关键字进行简单介绍。

1.override

override关键字的作用是检查派生类中函数是否在重写基类虚函数,如果不是重写基类虚函数,编译器就会报错。示例代码如下:

class Base     //基类Base 
{ 
public: 
 virtual void func(); 
 void show(); 
}; 
class Derive:public Base  //派生类Derive,公有继承Base类 
{ 
public: 
 void func() override;   //可通过编译 
 void show() override;   //不能通过编译 
}; 

在上述代码中,派生类Derive中func()函数后面添加override关键字可以通过编译,而show()函数后面添加override关键字,编译器会报错,这是因为show()函数并不是重写基类虚函数。

利用override关键字可以判断派生类是否准确地对基类虚函数进行重写,防止出现因书写错误而导致的基类虚函数重写失败。另外,在实际开发中,C++中的虚函数大多跨层继承,直接基类没有声明虚函数,但很可能会从“祖先”基类间接继承。如果类的继承层次较多或者类的定义比较复杂,那么在定义派生类时就会出现信息分散、难以阅读的问题,重写基类虚函数时,往往难以确定重写是否正确。此时,可以通过override关键字进行检查。

2.final

final关键字有两种用法:修饰类、修饰虚函数。当使用final关键字修饰类时,表示该类不可以被继承。示例代码如下:

class Base final    //final修饰类,Base类不能被继承 
{ 
public: 
 //... 
}; 
class Derive :public Base     //编译错误 
{ 
public: 
 //... 
}; 

在上述代码中,Base类被final关键字修饰,就不能作为基类派生新类,因此当Derive类继承Base类时,编译器会报错。

除了修饰类,final关键字还可以修饰虚函数,当使用final关键字修饰虚函数时,虚函数不能在派生类中重写。示例代码如下:

class Base 
{ 
public: 
 virtual void func() final; 
}; 
class Derive:public Base 
{ 
public: 
 void func();      //不能通过编译
}; 

在上述代码中,Derive类公有继承Base类,在Derive类中重写基类被final修饰的虚函数func()时,编译器会报“无法重写‘final’函数Base::func()”的错误。

5.2.2 虚函数实现多态的机制

在编写程序时,我们需要根据函数名、函数返回值类型、函数参数等信息正确调用函数,这个匹配过程通常称为绑定。C++提供了两种函数绑定机制:静态绑定和动态绑定。静态绑定也称为静态联编、早绑定,它是指编译器在编译时期就能确定要调用的函数。动态绑定也称为动态联编、迟绑定,它是指编译器在运行时期才能确定要调用的函数。

虚函数就是通过动态绑定实现多态的,当编译器在编译过程中遇到virtual关键字时,它不会对函数调用进行绑定,而是为包含虚函数的类建立一张虚函数表Vtable。在虚函数表中,编译器按照虚函数的声明顺序依次保存虚函数地址。同时,编译器会在类中添加一个隐藏的虚函数指针VPTR,指向虚函数表。在创建对象时,将虚函数指针VPTR放置在对象的起始位置,为其分配空间,并调用构造函数将其初始化为虚函数表地址。需要注意的是,虚函数表不占用对象空间。

派生类继承基类时,也继承了基类的虚函数指针。当创建派生类对象时,派生类对象中的虚函数指针指向自己的虚函数表。在派生类的虚函数表中,派生类虚函数会覆盖基类的同名虚函数。当通过基类指针或基类引用操作派生类对象时,以操作的对象内存为准,从对象中获取虚函数指针,通过虚函数指针找到虚函数表,调用对应的虚函数。

下面结合代码分析虚函数实现多态的机制,示例代码如下:

class Base1       //定义基类Base1 
{ 
public: 
 virtual void func();    //声明虚函数func() 
 virtual void base1();    //声明虚函数base1()
 virtual void show1();    //声明虚函数show1()
}; 
class Base2       //定义基类Base2 
{ 
public: 
 virtual void func();    //声明虚函数func() 
 virtual void base2();    //声明虚函数base2()
 virtual void show2();    //声明虚函数show2()
}; 
//定义Derive类,公有继承Base1和Base2 
class Derive :public Base1, public Base2 
{ 
public: 
 virtual void func();    //声明虚函数func() 
 virtual void base1();    //声明虚函数base1() 
 virtual void show2();    //声明虚函数show2() 
}; 

在上述代码中,基类Base1有func()、base1()和show1()三个虚函数;基类Base2有func()、base2()和show2()三个虚函数;派生类D erive公有继承Base1和Base2,D erive类声明了func()、base1()和show2()三个虚函数。D erive类与Base1类和Base2类的继承关系如图5-2所示。

在编译时,编译器发现Base1类与Base2类有虚函数,就为两个类创建各自的虚函数表,并在两个类中添加虚函数指针。如果创建Base1类对象(如base1)和Base2类对象(如base2),则对象中的虚函数指针会被初始化为虚函数表的地址,即虚函数指针指向虚函数表。对象base1与对象base2的内存逻辑示意图如图5-3所示。

图5-2 Derive类与Base1类和Base2类的继承关系

图5-3 对象base1与对象base2的内存逻辑示意图

D erive类继承自Base1类与Base2类,也会继承两个基类的虚函数指针。D erive类的虚函数func()、base1()和show2()会覆盖基类的同名虚函数。如果创建D erive类对象(如derive),则对象derive的内存逻辑示意图如图5-4所示。

图5-4 对象derive的内存逻辑示意图

通过基类Base1、基类Base2的指针或引用操作D erive类对象,在程序运行时,编译器从D erive类对象内存中获取虚函数指针,通过指针找到虚函数表,调用相应的虚函数。不同的类,其函数实现都不一样,在调用时就实现了多态。

5.2.3 虚析构函数

在C++中不能声明虚构造函数,因为构造函数执行时,对象还没有创建,不能按照虚函数方式调用。但是,在C++中可以声明虚析构函数,虚析构函数的声明是在“~”符号前添加virtual关键字,格式如下所示:

virtual ~析构函数(); 

在基类中声明虚析构函数之后,基类的所有派生类的析构函数都自动成为虚析构函数。

在基类声明虚析构函数之后,使用基类指针或引用操作派生类对象,在析构派生类对象时,编译器会先调用派生类的析构函数释放派生类对象资源,然后再调用基类析构函数。如果基类没有声明虚析构函数,在析构派生类对象时,编译器只会调用基类析构函数,不会调用派生类析构函数,导致派生类对象申请的资源不能正确释放。

下面通过案例演示虚析构函数的定义与调用,如例5-2所示。

例5-2 vdestructor.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3 class Base       //基类Base 
 4  { 
 5  public: 
 6       virtual ~Base();     //虚析构函数 
 7  }; 
 8  Base::~Base() 
 9  { 
 10      cout<<"Base类析构函数"<<endl;  
 11 } 
 12 class Derive:public Base     //派生类Derive,公有继承Base类 
 13 { 
 14 public: 
 15      ~Derive();       //虚析构函数 
 16 }; 
 17 Derive::~Derive() 
 18 { 
 19      cout<<"Derive类析构函数"<<endl;  
 20 } 
 21 int main() 
 22 { 
 23      Base *pb=new Derive;     //基类指针指向派生类对象 
 24      delete pb;       //释放基类指针 
 25      return 0; 
 26 } 

例5-2运行结果如图5-5所示。

图5-5 例5-2运行结果

在例5-2中,第3~7行代码定义了Base类,该类声明了虚析构函数。第12~16行代码定义D erive类公有继承Base类。D erive类中定义了析构函数,虽然析构函数前面没有添加关键字virtual,但它仍然是虚析构函数。第23~24行代码,定义了Base类指针pb指向一个D erive类对象,然后使用delete运算符释放pb指向的空间。

由图5-5可知,程序先调用了D erive类析构函数,然后又调用了Base类析构函数。

虚析构函数的定义与用法很简单,但在C++程序中却是非常重要的一个编程技巧。在编写C++程序时,最好把基类的析构函数声明为虚析构函数,即使基类不需要析构函数,也要显式定义一个函数体为空的虚析构函数,这样所有派生类的析构函数都会自动成为虚析构函数。如果程序中通过基类指针释放派生类对象,编译器能够调用派生类的析构函数完成派生类对象的释放。

5.3 纯虚函数和抽象类

有时候在基类中声明函数并不是基类本身的需要,而是考虑到派生类的需求,在基类中声明一个函数,函数的具体实现由派生类根据本类的需求定义。例如,动物都有叫声,但不同的动物叫声不同,因此基类(动物类)并不需要实现描述动物叫声的函数,只需要声明即可,函数的具体实现在各派生类中完成。在基类中,这样的函数可以声明为纯虚函数。

纯虚函数也通过virtual关键字声明,但是纯虚函数没有函数体。纯虚函数在声明时,需要在后面加上“=0”,格式如下所示:

virtual 函数返回值类型 函数名(参数列表) =  0;

上述格式中,纯虚函数后面“=0”并不是函数的返回值为0,它只是告诉编译器这是一个纯虚函数,在派生类中会完成具体的实现。

纯虚函数的作用是在基类中为派生类保留一个接口,方便派生类根据需要完成定义,实现多态。派生类都应该实现基类的纯虚函数,如果派生类没有实现基类的纯虚函数,则该函数在派生类中仍然是纯虚函数。

如果一个类中包含纯虚函数,这样的类称为抽象类。抽象类的作用主要是通过它为一个类群建立一个公共接口(纯虚函数),使它们能够更有效地发挥多态性。抽象类声明了公共接口,而接口的完整实现由派生类定义。

抽象类只能作为基类派生新类,不能创建抽象类的对象,但可以定义抽象类的指针或引用,通过指针或引用操作派生类对象。抽象类可以有多个纯虚函数,如果派生类需要实例化对象,则在派生类中需要全部实现基类的纯虚函数。如果派生类没有全部实现基类的纯虚函数,未实现的纯虚函数在派生类中仍然是纯虚函数,则派生类也是抽象类。

下面通过案例演示纯虚函数和抽象类的应用,如例5-3所示。

例5-3 abstract.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3 class Animal      //动物类Animal 
 4  { 
 5  public: 
 6       virtual void speak()=0;    //纯虚函数speak() 
 7       virtual void eat()=0;   //纯虚函数eat() 
 8       virtual ~Animal();     //虚析构函数 
 9  }; 
 10 Animal::~Animal() 
 11 { 
 12      cout<<"调用Animal析构函数"<<endl; 
 13 } 
 14 class Cat:public Animal    //猫类Cat,公有继承Animal类 
 15 { 
 16 public: 
 17      void speak();      //声明speak()函数 
 18      void eat();     //声明eat()函数 
 19      ~Cat();      //声明析构函数 
 20 }; 
 21 void Cat::speak()     //实现speak()函数 
 22 { 
 23      cout<<"小猫喵喵叫"<<endl; 
 24 } 
 25 void Cat::eat()     //实现eat()函数 
 26 { 
 27      cout<<"小猫吃鱼"<<endl; 
 28 } 
 29 Cat::~Cat()      //实现析构函数 
 30 { 
 31      cout<<"调用Cat析构函数"<<endl; 
 32 } 
 33 class Rabbit:public Animal    //兔子类Rabbit,公有继承Animal类 
 34 { 
 35 public: 
 36      void speak();      //声明speak()函数 
 37      void eat();     //声明eat()函数 
 38      ~Rabbit();      //声明析构函数 
 39 }; 
 40 void Rabbit::speak()    //实现speak()函数 
 41 { 
 42      cout<<"小兔子咕咕叫"<<endl; 
 43 } 
 44 void Rabbit::eat()     //实现eat()函数 
 45 { 
 46      cout<<"小兔子吃白菜"<<endl; 
 47 } 
 48 Rabbit::~Rabbit()     //实现析构函数 
 49 { 
 50      cout<<"调用Rabbit析构函数"<<endl; 
 51 } 
 52 int main() 
 53 { 
 54      Animal* pC=new Cat;    //定义基类指针pC指向Cat类对象 
 55      pC->speak();      //通过pC指针调用Cat类的speak()函数 
 56      pC->eat();      //通过pC指针调用Cat类的eat()函数 
 57      delete pC;      //释放pC指针指向的空间 
 58      Animal* pR=new Rabbit;    //定义基类指针pR指向Rabbit类对象 
 59      pR->speak();      //通过pR指针调用Rabbit类的speak()函数 
 60      pR->eat();      //通过pR指针调用Rabbit类的eat()函数 
 61      delete pR;      //释放pR指针指向的空间 
 62     return 0; 
 63 } 

例5-3运行结果如图5-6所示。

在例5-3中,第3~9行代码定义了动物类Anim al,该类提供了speak()和eat()两个纯虚函数。第14~20行代码定义了猫类Cat公有继承Anim al类,Cat类实现了Anim al类的全部纯虚函数。第33~39行代码定义了兔子类Rabbit公有继承Anim al类,Rabbit类实现了Anim al类的全部纯虚函数。第54~57行代码,在m ain()函数中定义了Anim al类指针pC指向一个Cat类对象,并通过pC指针调用speak()函数和eat()函数,之后使用delete运算符释放pC指针指向的空间。第58~61行代码,定义Anim al类指针pR指向一个Rabbit类对象,并通过pR指针调用speak()函数和eat()函数,之后使用delete运算符释放pR指针指向的空间。

图5-6 例5-3运行结果

由图5-6可知,Cat类和Rabbit类对象创建成功,并且通过基类指针pC和pR成功调用了各派生类的函数,实现了多态。在释放指针指向的空间时,先调用了派生类的析构函数,后调用了基类析构函数。

在例5-3中,Anim al类是抽象类,如果创建Anim al类对象,编译器会报错,例如在m ain()函数中添加如下代码:

Animal animal;

再次运行例5-3的程序,编译器会报错,如图5-7所示。

如果Anim al类的某个派生类没有全部实现纯虚函数,则派生类也是抽象类,不能创建该派生类的对象。

图5-7 不能创建Animal类对象

【阶段案例】停车场管理系统

一、案例描述

停车场管理系统是模拟停车场进行车辆管理的系统,该系统分为汽车信息、普通用户和管理员用户三个模块。各个模块的具体功能如下所示。

1. 汽车信息模块

(1)添加汽车信息:添加汽车属性信息。

(2)删除汽车信息:输入停车场中存在的车牌号,删除汽车信息。

(3)查找汽车信息:输入停车场中存在的车牌号,显示汽车详细信息。

(4)修改汽车信息:输入停车场中存在的车牌号,修改汽车属性信息。

(5)停车时长统计:显示汽车在停车场中停留的时间。

(6)停车场信息显示:显示停车场所有汽车的属性信息。

(7)汽车信息保存:将汽车信息保存到本地文件中。

2. 普通用户模块

(1)显示汽车信息:查看汽车信息和停车费信息。

(2)查询汽车信息:查询汽车详细信息。

(3)停车时间统计:查看汽车停留时间。

(4)退出普通用户登录:返回主菜单。

3. 管理员用户模块

(1)添加汽车信息:添加新的汽车信息。

(2)显示汽车信息:显示所有汽车信息。

(3)查询汽车信息:通过车牌号查询汽车信息。

(4)修改汽车信息:修改汽车属性信息。

(5)删除汽车信息:删除指定车牌号的汽车信息。

(6)汽车信息统计:显示停车场中所有汽车停留时长。

(7)退出管理员用户登录:返回主菜单。

二、案例分析

通过停车场管理系统案例描述可知,停车场管理系统需要展现一个菜单界面,用户可以根据选项进入普通用户和管理员用户界面,普通用户只能查看汽车信息,只有管理员用户才能添加、修改、删除汽车的信息。普通用户和管理员用户具备返回主菜单的功能。下面根据模块的功能为每个模块设计类。

1. 汽车信息管理类设计

根据案例描述,针对汽车信息模块可以设计一个类Car,汽车属性设计为成员变量,汽车信息模块的功能设计为成员函数。Car类的详细设计如图5-8所示。

Car类中的汽车信息属性包括汽车牌号、汽车类型、汽车颜色和进入停车场的时间。其中,车牌号、汽车类型和汽车颜色定义为string类型,汽车进入停车场的时间定义为tim e_t类型。Car类的成员函数分别如下所示:

(1)添加汽车信息——addCar()

添加汽车属性信息,当有汽车入库时需要对车辆信息进行录入,即存储车牌号、汽车类型、汽车颜色和汽车进入停车场的时间。停车场中的车辆信息以文件的形式进行存储,添加完成后,保存文件。

(2)删除汽车信息——delCar()

删除停车场的汽车信息,通过输入车牌号查找到车辆并删除该车的信息。delCar()函数的实现是通过读取保存汽车信息的文件,在读文件时,不删除的汽车信息被写入一个临时文件,而被删除的汽车信息不写入。文件读写完毕,再将临时文件中的数据写入原文件。

图5-8 Car类详细设计

(3)查找汽车信息——findCar()

查找汽车信息通过输入车牌号进行,读取本地存储的汽车信息文件后,若找到与输入的车牌号匹配的记录,则显示要查找的汽车信息。

(4)修改汽车信息——m odCar()

修改汽车信息需要输入车牌号进行,以修改文件的形式完成。m odCar()函数的实现也是通过读取保存车辆信息的文件,在读文件时,将文件信息连同修改后的汽车信息写入一个临时文件。文件读写完毕,再将临时文件中的数据写入原文件。

(5)停车时长统计——tim eAm ount()

在添加汽车信息的时候将系统当前时间写入文件中,再次查看时,利用系统当前时间减去写入文件时的时间得到停车时长。根据停车时长分别计算停车超过一天和停车不足一天的车辆数。

(6)停车场信息显示——showInfor()

通过读取文件的方式显示停车场所有的汽车信息,并显示停车费用。

(7)汽车信息保存——saveInfor()

将新添加的汽车信息保存到文件中。

2. 普通用户类设计

普通用户只能查看停车场的汽车信息,不具备修改汽车信息的权限。针对普通用户模块可以设计一个普通用户类U ser,U ser类的详细设计如图5-9所示。

普通用户通过菜单选择要查看的信息,用户可以执行的操作分别是显示所有汽车信息、查询汽车信息、统计汽车信息和返回主菜单的功能。通过输入不同的选项值执行不同的操作。

3. 管理员用户类设计

管理员用户除了具备普通用户的功能,还具备修改、添加、删除汽车信息的功能。针对管理员用户模块可以设计一个管理员用户类Adm in,Adm in类的详细设计如图5-10所示。

图5-9 User类详细设计

图5-10 Admin类详细设计

三、案例实现

1. 实现思路

面向对象的核心思想之一就是对事物进行抽象,将事物封装成为类。对案例需求进行详细分析后,将需求抽象成三个具体的类。首先抽象出汽车类Car,专门用于存储汽车属性信息、实现汽车信息的删除和查找等功能;其次抽象出普通用户类U ser,普通用户只具有对停车场信息查询的权限;最后抽象出管理员类Adm in,管理员用户对停车场中的汽车信息可以进行修改、添加、删除等操作。

(1)根据需求分析中的类设计图,分别实现汽车类Car、普通用户类U ser和管理员类Adm in。

(2)在主函数m ain()中,绘制系统主菜单,通过提示输入选项进行不同的操作,可以选择普通用户和管理员用户。

2. 完成代码

请扫描右侧二维码,查看停车场管理系统的具体实现。

5.4 本章小结

本章主要讲解了多态与虚函数的相关知识,首先讲解了多态的概念与实现条件;其次讲解了虚函数实现多态,包括虚函数的概念与作用、虚函数实现多态的机制和虚析构函数;然后讲解了纯虚函数和抽象类;最后综合前面几章所学知识开发了一个停车场管理系统,强化读者对C++基础知识的掌握,并加深读者对面向对象程序设计思想的理解。学习完本章内容,读者已经了解了面向对象程序设计中的封装、继承、多态三大机制,初窥了面向对象程序设计思想的精髓,通过本章的学习必将开启探索更高层次知识的大门。

5.5 本章习题

一、填空题

1. 虚函数使用___关键字声明。

2. 关键字___可以检测派生类对基类虚函数的重写是否正确。

3.C++中的多态包括___和___两种。

4. 声明了纯虚函数的类称为___。

二、判断题

1. 函数重载也是C++中的一种多态实现形式。( )

2. 构造函数可以声明为虚函数。( )

3. 虚函数可以声明为类的静态成员函数。( )

4. 虚函数实现的多态是在编译时期确定的。( )

5. 纯虚函数要在类内声明,类外实现。( )

6. 声明了纯虚函数的类可以创建对象。( )

三、选择题

1. 关于多态,下列说法中错误的是( )。

A.继承中的多态是动态多态

B.多态的本质是指函数的多种实现形态

C.由重载函数实现的多态是编译时多态

D.动态多态是由纯虚函数实现的

2. 关于虚函数,下列说法中错误的是( )。

A.虚函数是运行时多态

B.声明虚函数的方法就是在成员函数原型前添加virtual关键字

C.虚函数只能是类中的成员函数,但不可以是静态成员函数

D.派生类对基类虚函数重写时,可以不必与基类中虚函数的原型完全一致

3. 关于虚函数实现多态的机制,下列说法中错误的是( )。

A.虚函数是通过动态绑定实现多态的

B.在编译包含虚函数的类时,编译器会在类中添加一个隐藏的虚函数指针,指向虚函数表C.创建对象时,虚函数指针不占用对象内存空间

D.在继承时,派生类会继承基类的虚函数指针

4. 阅读下列程序:

#include<iostream> 
using namespace std; 
class A { 
public: 
    A() { cout << 'A'; } 
    ~A() { cout << 'C'; } 
}; 
class B : public A { 
public: 
    B() { cout << 'G'; } 
    ~B() { cout << 'T'; } 
}; 
int main() 
{ 
    B obj; 
    return 0; 
} 

程序的运行结果为( )。

A.GATC

B.AGTC

C.GACT

D.AGCT

5. 关于虚析构函数,下列说法中错误的是( )。

A.虚析构函数的声明形式也是在析构函数声明前使用virtual关键字

B.在基类中声明虚析构函数之后,基类的所有派生类的析构函数都自动成为虚析构函数

C.声明虚析构函数之后,析构派生类对象时,会先调用基类虚析构函数再调用派生类析构函数

D.虚析构函数的名称可以与类名不相同

四、简答题

1. 简述一下C++中的多态。

2. 简述一下虚函数实现多态的原理。

五、编程题

请设计一个描述形状的类Shape,Shape类中有两个成员函数:

(1)getArea()函数:用于计算形状的面积。

(2)getLen()函数:用于计算形状的周长。

Shape类有两个派生类:

(1)Rectangle类:表示矩形。

(2)Circle类:表示圆形。

请编写程序实现以下功能:

(1)计算不同边长的矩形的面积和周长。

(2)计算不同半径的圆的面积和周长。