虚函数
虚函数是运行时多态,若某个基类函数声明为虚函数,则其公有派生类将定义与其基类虚函数原型相同的函数,这时,当使用基类指针或基类引用操作派生类对象时,系统会自动用派生类中的同名函数代替基类虚函数。
在前面的内容中曾提到,若派生类中定义了与基类同名的函数,使用基类指针指向派生类对象时,通过指针调用的是基类函数而不是派生类中的函数。
接下来通过一个案例来验证基类指针指向派生类对象时,基类指针调用基类函数的情况,如例1所示。
例1
1 #include <iostream>
2 using namespace std;
3
4 class Animal //定义基类Animal
5 {
6 public:
7 //基类speak()函数
8 void speak(){ cout << "animal language!" << endl; }
9 };
10 class Cat:public Animal //定义派生类Cat
11 {
12 public:
13 //定义与基类同名的函数speak()
14 void speak(){ cout << "cat language: miaomiao!" << endl; }
15 };
16 int main()
17 {
18 Cat cat; //定义派生类对象cat
19 Animal *panimal = &cat; //定义基类指针并指向派生类对象
20 panimal->speak(); //通过指针调用speak()函数
21 system("pause");
22 return 0;
23 }
程序运行结果如图1所示。
图1 例1运行结果
例1中定义了基类Animal和公有继承派生类Cat,在基类中定义了speak()函数用于描述动物都能发出叫声这样的行为,派生类Cat也定义了speak()函数用于描述猫特有的叫声。
main()函数定义了派生类对象cat,基类指针panimal并初始化为cat地址。运行结果显示,通过panimal指针访问了基类speak()成员,并没有调用派生类中的speak()函数,不是用户想要的结果。
派生类根据自身要求,继承并改写了基类同名函数speak(),这种改变在静态联编的条件下编译器并不知道,造成了上述结果,若想通知编译器这种改变,则需要通过动态联编实现,其方法就是在基类中将可能发生改变的成员函数声明为虚函数。
声明虚函数的方法就是在成员函数原型前添加virtual关键字,具体的声明形式如下所示:
class 类名
{
virtual 函数返回值类型 函数名(参数表)
};
对于虚函数的声明有以下几点需要注意:
1、 虚函数只能是类中的函数,但不可以是静态成员函数。
2、 派生类对基类虚函数重新定义时,必须与基类中虚函数的原型完全一致,包括返回值类型,函数名,参数个数,参数类型及参数顺序。派生类中同名函数前是否添加virtual,均被视为虚函数。
本节中将讨论两种虚函数:一般虚函数成员、虚析构函数。
1、一般虚函数成员
对于普通成员函数,派生类可以重新定义从基类继承下来的虚函数,从而形成该函数在派生类中的专门版本。派生类对基类虚函数重新定义后,仍作为虚函数可在更下层派生类中被重新定义。通常,在派生类中重新定义虚函数时,virtual可以不出现,但最好保留,以增强程序的可读性。
有了虚函数后,通过基类指针或基类引用调用派生类对象的虚函数时,会实际调用指针或引用指向的派生类对象中那个重定义版本,即操作派生类的虚函数。
接下来通过案例改写上一节案例,将基类成员函数speak()定义为虚函数并在派生类Cat中重新实现,如例2所示。
例2
1 #include <iostream>
2 using namespace std;
3
4 class Animal //定义基类Animal
5 {
6 public:
7 //定义虚函数speak()
8 virtual void speak(){ cout << "animal language!" << endl; }
9 };
10 class Cat:public Animal //定义派生类Cat
1 {
2 public:
3 //定义Cat类自己的虚函数speak()
4 virtual void speak(){ cout << "cat language: miaomiao!" << endl; }
5 };
6 int main()
7 {
8 Cat cat; //定义派生类对象cat
9 Animal *panimal = &cat; //定义基类指针并初始化为cat地址
10 Animal &ref = cat; //定义基类引用,初始化为cat
11 panimal->speak(); //通过panimal指针调用speak()函数
12 ref.speak(); //通过引用ref调用speak()函数
13 system("pause");
14 return 0;
15 }
运行结果如图2所示。
图2 例2运行结果
例2中第8行定义基类虚函数speak(),第14行派生类Cat重新定义了该函数。在main()函数中第19、20行定义了基类指针和引用,但指针保存的是派生类对象地址,引用的是派生类对象,通过指针和引用访问speak()函数时操作的是派生类对象的函数。
看到了虚函数对运行结果产生的影响后,接下来了解一下虚函数的实现机制。从多态概念一节的内容了解到,虚函数通过动态联编实现了运行时多态,编译器在执行过程中遇到virtual关键字时,会为这些包含虚函数的类建立一张虚函数表vtable。在虚函数表中,编译器将按照虚函数的声明顺序依次保存虚函数地址,同时在每个带有虚函数的类中放置一个vptr指针,用来指向虚函数表。通常在定义类对象时,为vptr分配空间,该指针被置于对象的起始位置,继而通过对象的构造函数将vptr初始化为本类的虚函数表地址。
根据以上说明,可以对例2中Cat类对象cat的虚函数表进行描述,内容如图3所示。
图3 cat对象的虚函数表
图3也许还不能明确说明在派生类中重新定义了基类同名虚函数时,基类指针指向派生类对象时调用派生类自身虚函数的工作原理。接下来一步一步进行分析,首先在基类和派生类中定义不同的虚函数,假设有如下代码:
class Animal //基类Animal
{
public:
//虚函数speak()
virtual void speak(){ cout << "animal language!" << endl; }
//虚函数sleep()
virtual void sleep(){ cout << "animal sleep!" << endl; }
};
class Cat:public Animal //派生类Cat
{
public:
//虚函数cat_speak()
virtual void cat_speak(){ cout << "cat language: miaomiao!" << endl; }
//虚函数cat_sleep()
virtual void cat_sleep(){ cout << "cat sleep!" << endl; }
};
上述代码中,基类Animal中有两个虚函数,派生类Cat也有两个虚函数,派生类中的虚函数名称与基类虚函数名称不同,不是基类中虚函数的重新定义。若定义派生类Cat对象cat则它的虚函数表如图4所示。
图4 基类、派生类中不同名虚函数对应的虚函数表
从图4看到,虚函数按照其声明顺序列于表中并且基类的虚函数在派生类虚函数之前。修改上述代码,派生类中定义与基类同名的虚函数speak()。只给出派生类定义,代码如下所示:
class Cat:public Animal //定义派生类Cat
{
public:
//定义基类同名虚函数speak()
virtual void speak(){ cout << "cat language: miaomiao!" << endl; }
//定义虚函数cat_sleep()
virtual void cat_sleep(){ cout << "cat sleep!" << endl; }
};
按照上述代码,若定义派生类对象,则对象的虚函数表如图5所示。
图5 派生类中重写基类虚函数后的虚函数表
从图5可以看到,派生类的speak()函数被放到了虚函数表中原来基类虚函数的位置,没有被覆盖的函数依旧。因此可以看出,通过基类Animal指针操作派生类Cat对象的speak()函数时,找到的是虚函数表描述的派生类成员函数,正是我们想访问的内容。
下面总结一下带有虚函数时,C++编译器的操作步骤:
1、为各个类建立虚函数表,若无虚函数则不操作。
2、暂不连接虚函数,只是将各个虚函数地址放入虚函数表。
3、连接各静态函数。
2、虚析构函数
在C++中,不能声明虚构造函数,因为构造函数执行时,对象还没有构造好,不可按虚函数方式进行调用,但可以声明虚析构函数。
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针销毁派生类对象的应用产生的。通常,使用基类指针指向一个new生成的派生对象,通过delete销毁基类指针指向的派生类对象时,有以下两种情况:
1、 如果基类析构函数不是虚析构函数,则只会调用基类的析构函数,派生类的析构函数不被调用,此时派生类中申请的资源不被回收。
2、 如果基类析构函数为虚析构函数,则释放基类指针指向的对象时会调用基类及派生类析构函数,派生类对象中的所有资源被回收。
虚析构函数的声明形式也是在析构函数名前使用virtual关键字,具体声明形式如下所示:
virtual ~类名();
接下来通过一个案例说明虚析构函数的定义及操作,如例3所示。
例3
1 #include <iostream>
2 using namespace std;
3
4 class Animal //定义基类Animal
5 {
6 public:
7 Animal(char *name); //声明基类构造函数
8 void print_name(); //声明print_name()函数
9 virtual void print_color(); //声明虚函数print_color()
10 virtual void speak(); //声明虚函数speak()
11 virtual ~Animal(); //声明析构函数
12 private:
13 char *m_pAnimalName; //存放动物名称的数据成员
14 };
15 Animal::Animal(char *name) //Animal类构造函数的定义
16 {
17 int len = strlen(name) + 1;
1 m_pAnimalName = new char[len]; //为m_pAnimalName指针开辟空间
2 strcpy_s(m_pAnimalName, len, name); //存入动物名称
3 }
4 Animal::~Animal() //Animal类析构函数
5 {
6 cout << "Animal destructor!" << endl;
7 if (m_pAnimalName){
8 delete[] m_pAnimalName; //释放空间
9 }
10 }
11 void Animal::print_name() //显示动物名称
12 {
13 cout << "name:" << m_pAnimalName << endl;
14 }
15 //定义虚函数print_color(),本函数在基类中为空函数,需要在派生类中重定义
16 void Animal::print_color()
17 {
18
19 }
20 void Animal::speak()
21 {
22 cout << "animal language!" << endl;
23 }
24
25 class Cat:public Animal //定义派生类Cat
26 {
27 public:
28 Cat(char* name, char *catcolor);
29 virtual void print_color(); //声明虚函数print_color()
30 virtual void speak(); //声明虚函数speak()
31 virtual ~Cat();
32 private:
33 char *m_pCatColor; //存放猫的颜色的数据成员
34 };
35 Cat::Cat(char* name, char *color):Animal(name) //Cat类构造函数的定义
36 {
37 cout << "Cat constructor!" << endl;
38 m_pCatColor = new char[strlen(color) + 1]; //为m_pCatcolor指针开辟空间
39 strcpy_s(m_pCatColor, strlen(color) + 1, color); //存入描述猫颜色自的字符串
40 }
41 Cat::~Cat() //Cat类析构函数的定义
42 {
43 cout << "Cat destructor!" << endl;
44 if (m_pCatColor){
45 delete[] m_pCatColor; //释放m_pCatcolor指向的空间
46 }
47 }
48 void Cat::print_color() //print_color()虚函数的实现
49 {
50 cout << "cat color :" << m_pCatColor << endl;
51 }
52 void Cat::speak() //speak()虚函数的实现
53 {
54 cout << "cat language: miaomiao!" << endl;
55 }
56 int main()
57 {
58 Animal *p[2]; //定义基类Animal指针数组
59 int i;
60 p[0] = new Cat("short_haired_cat", "white");//通过new生成派生类Cat对象
61 p[0]->print_name();
62 p[0]->print_color();
63 p[0]->speak();
64 p[1] = new Cat("persian_cat", "brown"); //通过new生成派生类Cat对象
65 p[1]->print_name();
66 p[1]->print_color();
67
68 for (i = 0; i < 2; i++)
69 delete p[i]; //通过delete释放派生类对象
70 system("pause");
71 return 0;
72 }
程序运行结果如图6所示。
图6 例3运行结果
例3中定义了基类Animal和派生类Cat,将基类和派生类中的析构函数都声明为虚析构函数。在基类的析构函数中释放了构造函数开辟的用于存放动物名称的空间,派生类的析构函数释放了用于存放动物颜色的空间。
main()函数中定义了基类指针数组p,数组成员记录的是通过new生成的派生类对象地址,在delete时自动调用析构函数并且基类和派生类的析构函数均为虚函数,因此基类和派生类的析构函数均被调用,资源全部被释放。
还需额外说明的是,基类中的print_color()函数需要定义,因为若该函数不存在,在main()函数中通过基类指针访问print_color()将出错。基类中该函数提供了一个接口,用于显示动物颜色,具体实现在派生类中按照不同动物颜色重新定义。类似于本函数,若存在需要基类中提供接口,在派生类中实现操作的需求,C++中将通过定义纯虚函数来实现,具体内容在下节介绍。
若修改上述代码,在基类和派生类中定义普通析构函数,去掉前面的virtual关键字,则程序的运行结果如图7所示。
图7 析构函数为普通函数时程序运行结果
从运行结果看到,若析构函数不是虚函数,则delete基类指针指向的内容时,将只调用基类析构函数,派生类中申请的资源将不会被释放。