【正确答案】
【答案解析】指向基类的指针在操作它的多态类对象时,会根据不同的类对象调用其相应的函数,这个函数就是虚函数,虚函数用virtual修饰函数名。虚函数的作用是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数进行重新定义。在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型(参数类型的顺序也要一致),以实现统一的接口。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
在基类的类定义中定义虚函数的一般形式如下:
class<类名>
{
virtual函数返回值类型虚函数名(形参表);
...
};
下述程序示例代码是一个虚函数的例子。
#include<iostream>
using namespace std;
class A
{
public:
virtual void Print()
{
printf("This is Class A/n");
}
};
class B:public A
{
public:
void Print()
{
printf("This is Class B/n!");
}
};
int main()
{
A a:
B b:
A* p1=&a;
A* p2=&b;
p1->Print();
p2->Print();
return 0;
}
程序输出结果:
This is Class A
This is Class B
需要注意的是,虚函数虽然非常好用,但是在使用虚函数时,并非所有的函数都需要定义成虚函数,因为实现虚函数是有代价的。在使用虚函数时,需要注意以下几个方面的内容:
1)只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。
2)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。
3)如果声明了某个成员函数为虚函数,则在该类中不能出现与这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。
4)非类的成员函数不能定义为虚函数,全局函数以及类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
5)普通派生类对象,先调用基类构造再调用派生类构造。
6)基类的析构函数应该定义为虚函数,这样可以在实现多态的时候不造成内存泄漏。基类析构函数未声明virmal,基类指针指向派生类时,delete指针不调用派生类析构函数。有virtual,则先调用派生类析构再调用基类析构。
7)基类指针动态建立派生类对象,普通调用派生类构造函数。
8)指针声明不调用构造函数。
虚函数的使用可以极大地提高软件开发的效率,那么虚函数是通过什么实现的呢?其实,虚函数是通过一张虚函数表(Virtual Table)来实现的。该表是一个类的虚函数的地址表,解决了继承、覆盖的问题,保证它能真实反应实际的函数。这样,在有虚函数的类的实例中,此表被分配在实例的内存中,所以当用父类的指针来操作一个子类的时候,这张虚函数表就显得非常重要,它指明了实际所应该调用的函数。
C++的编译器能够保证虚函数表的指针存在于对象实例中最前面的位置,通过对象实例的地址得到这张虚函数表,然后就可以遍历其中的函数指针,并调用相应的函数。例如,有这样的一个类:
class Base
{
public:
vinual void f() {cout<<"Base::f"<<endl;}
virtual void g() {cout<<"Base::g"<<endl;)
virtual void h(){cout<<"Base::h"<<endl;}
};
可以通过Base的实例来得到虚函数表。程序示例如下:
typedef void(*Fun)(void);
Base b;
Fun pFun=NULL;
cout<<"虚函数表地址:"<<(int*)(&b)<<endl;
cout<<"虚函数表—第一个函数地址:"<<(int*)*(int*)(&b)<<endl;
pFun=(Fun)*((int*)*(int*)(&b));
pFun();
实际运行结果:
虚函数表地址:0012FED4
虚函数表—第一个函数地址:0044F148
Base::f
通过这个示例可以看到,可以通过强行把&b转成int*,取得虚函数表的地址,然后再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int*强制转成了函数指针)。通过这个示例可以知道,调用Base::g()和Base::h()的代码如下:
(Fun)*((int*)*(int*)(&b)+0);∥Base::f()
(Fun)*((int*)*(int*)(&b)+1);∥Base::g()
(Fun)*((int*)*(int*)(&b)+2);∥Base::b()
应在构造函数中进行虚函数表的创建和虚函数指针的初始化。根据构造函数的调用顺序,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类对象的虚函数表的指针,该虚函数表指针指向父类的虚函数表。当执行子类的构造函数时,子类对象的虚函数表指针被初始化,指向自身的虚函数表。
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针vptr(对VC编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系起来,另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable。这样才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。