C++之虚函数详解
1、纯虚函数
虚函数的存在主要是为了方便C++中的多态特性,所谓的多态,简单的用一句话讲就是父类对象指向子类应用。在这篇博客中,我将介绍一下有关C++虚函数的知识,主要参考了下面这一篇博客文章,http://haoel.blog.51cto.com/313033/124595/。
虚函数我们主要有普通虚函数和纯虚函数,纯虚函数是没有实现体的,因此我们必须在其派生类中为其重新实现,包含纯虚函数的类称为虚基类,也就是我们经常说的抽象类,对于抽象类,我们不允许其实例化一个对象,必须被继承后才能使用。我们可以使用下面的方法来声明一个纯虚函数:
virtual void get()=0;
virtual作为关键字,表示虚拟的,我们声明一个虚函数就需要用这个关键字来修饰,需要注意的是,这就是我们纯虚函数的声明格式。
2、虚函数
下面我们就来着重介绍一下虚函数,我们知道,系统会为每个包含虚函数的类对象分配一个虚函数表和一个虚表指针,虚函数表中存放了虚函数的入口地址,虚表指针用于指向这个虚函数表,下图可以让我们更加直观了解该表。
下面我们就用程序来介绍一下该表的特征,该程序通过这个虚表指针来获取对应虚函数的地址,从而进行虚函数调用,程序代码如下:
#include <iostream>
using namespace std;
class Bass
{
public:
virtualvoid f(){cout << "f\n";}
virtualvoid g(){cout << "g\n";}
virtualvoid h(){cout << "h\n";}
};
typedef void (*Fun)();
//void (*test)(void);
int main()
{
Bass b;
Fun fun = NULL;
cout << "(int*)&b="<< (int*)&b << endl;
cout << "(int*)*(int*)&b"<< (int*)*(int*)&b<< endl;
cout << "(int*)*((int*)*(int*)(&b))"<< (int*)*((int*)*(int*)(&b)) << endl;
fun =(Fun)*((int*)*(int*)(&b));
fun();
fun =(Fun)*((int*)*(int*)(&b)+1);
fun();
fun =(Fun)*((int*)*(int*)(&b)+2);
fun();
//test =(void(*)())*(((int*)*(int*)&b));
//test();
return0;
}
在这段代码中,我们首先创建了一个包含三个虚函数的Bass类,然后在main函数中对其实例化一个对象,在解释这段代码之前,我们首先需要知道的是:在C++中,虚函数表的指针存在于对象实例内存的最前面位置。我们为每个实例对象分配内存,那么通过&b我们可以获取该类对象的起始地址,然后我们对其进行取值操作,也就是*(int*)(&b)时,我们就可以获取这块内存块最开始存放的地址了,这里就是我们的虚函数表地址了。我们可以用下面的图来表示这层关系。
这个图可能画的并非那么形象,但是大体上能解释这种地址之间的关系,我们也可以举一个可能不恰当的例子,对于一个二维数组a,我们可以通过a获取数组的首地址(因为数组名就代表其首地址,因此不需要加&),那么我们再用取值操作符*a)不就得到了第一个元素(我们将其当做一个整体来称呼吧)的值,但是第一个元素是一个一维数组,
因此,我们还可以继续对其取间接操作符*(*(a)),来获取这整个数组第一个元素的值,这其实跟我们这个虚函数表的访问过程是类似的,我们可以好好体会一下。
理解了上面的过程,那么我们就可以很清楚明白的写出如果获取一个虚函数表中的对应虚函数的地址了,也就是我们下面的三句话,分别是对应Bass类中的三个虚函数
*((int*)*(int*)(&b));
* ((int*)*(int*)(&b)+1);
*((int*)*(int*)(&b)+2);
看起来很麻烦,一步一步分析一下其实就很容易明白了。另外在程序中,我们用到了
typedef void (*Fun)();
这句话利用typedef定义了一个函数指针类型,类型名为Fun,我们可以类似使用int,float来使用他,如果不想用typedef,我们上面的注释掉的就是直接使用函数指针的方法。
好了,我们可以把上面的程序在VS2010上面运行一下,输出的结果为:
3、继承中的虚函数表特征
(1)子类不重写父类虚函数
下面我们就来介绍一下继承中子类重写父类虚函数时,虚函数表的结果,通过这个分析,我们将会明白函数调用之间的关系。
假如我们又声明了一个类Derive,他有三个虚函数,如果这三个虚函数分别为
virtual void f1(){…};
virtual void g1(){…};
virtual void h1(){…};
这时,子类没用重写父类的虚函数,因此,子类实例虚函数表就直接是下图所示:
子类虚函数按其声明顺序存放,且父类的虚函数放在虚函数表前面,下面我们也编个程序来看看验证一下,我们只需在上面代码的基础上添加一个新的类,代码如下:
class Derive:public Bass
{
public:
virtual voidf1(){cout << "f1\n";}
virtual voidg1(){cout << "g1\n";}
virtual voidh1(){cout << "h1\n";}
};
然后将main函数中的内容替换成:
Derive d;
Fun fun = NULL;
fun = (Fun)*((int*)*(int*)(&d)); (1)
fun();
fun = (Fun)*((int*)*(int*)(&d)+3); (2)
fun();
通过我们上面的分析,我们知道,(1)取的是Bass::f()函数的地址,(2)取的是Derive::f1()函数的地址, 因此打印输出为
f
f1
在VS2010下测试打印出了分析的结果,在此大家可以自行测试一下。
(2)子类重写父类虚函数
上面的情况中子类没有重写父类虚函数,下面我们就来介绍子类重写了父类虚函数时子类虚函数表的特征。假如我们声明的Derive类中虚函数时下面这样:
virtual void f (){…};
virtual void g1(){…};
virtual void h1(){…};
我们在子类中也有一个void f(){}函数,那么这个时候,子类的虚函数表是如何分布的内?
这就是子类的虚函数表,我们发现其父类中被子类重载的函数在表中被子类的重载函数给替换了,我们可以很轻易通过程序来验证我们的分析是否正确。
4、多重继承中的虚函数表特征
对于多重继承,也存在子类重写或者不重写父类的情况,下面我们也分两种情况来说明。
(1)子类不重写父类虚函数
我们可以重新定义两个类Bass2,Bass3,我们定义其成员函数都是
virtual void f (){…};
virtual void g (){…};
virtual void h (){…};
这时,我们的Derive类按顺序分别继承了Bass,Bass2,Bass3,则这个时候的Derive实例对象的虚函数表图示如下:
这个图是从网上借鉴过来的,因为自己画感觉没这么形象,所以就借用这个了,我们发现多重继承下,子类的虚函数表像一个二维的表,子类的虚函数在第一个继承的父类表的后面,然后其它的父类函数则在下一个维度的空间,这样做的好久就是避免了多重继承造成的结构紊乱,解决不同的父类类型的指针指向同一个子类实例的时候能够调用到实际的函数,我们可以用程序来验证这个分析。
将main函数替换成下面代码
Derive d;
Bass1 *b1 = &d;
Bass2 *b2 = &d;
Bass2 *b3 = &d;
b1->f();
b2->f();
b3->f();
这段代码实现了虚函数的多态特性,根据分析,我们应该打印的结果分别是
Base1::f()函数的输出内容
Base2::f()函数的输出内容
Bass3::f()函数的输出内容
(2)子类重写父类虚函数
假如子类中重载了父类中的虚函数,会是什么样内?比方说我们将Derive中的第一个虚函数改为
virtual void f()
那么虚函数表就是下面图示了,
我们可以看到,虚函数表组中,子类重载的父类函数全都被子类重写的函数覆盖了,我们依然可以用程序来验证这个结果,还是上面的那个程序,则我们打印的结果就该是三个Derive::f()函数的输出结果了,这里如果按照上面的定义的话,那就是
f1
f1
f1
需要说明的是,如果父类中的虚函数是private或者protected,我们就只能通过虚函数表的方式来访问了,而不能通过对象来访问。就像我们刚开始介绍的那种方法。
我们提到过,多重继承时,我们可以把这个虚函数表当成一个二维数组,或者说是一个指针数组,那么我们可以还可以采用下面的方法来调用我们的虚函数。
Derive d;
Fun fun = NULL;
int** pVtb = (int**)&d;
fun = (Fun)pVtb[0][1];
fun();
或者我们可以将上面的第三四句替换成下面这句话:
fun =(Fun)*((int*)(*(int*)(&d)+0)+ 1);
在VS2010上面运行,可以得到正确的结果。