虚函数是C++一大核心技术,主要是为了实现多态而设计的。什么是多态,字面意思就是多种形态,是一种泛型,就是以不变的代码表示可变的行为,分为编译时多态和运行时多态。比如模板技术、函数重载可以认为是编译时多态,虚函数则是运行时多态。
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
/*******************************************************************************
函 数 名 :
功能描述 : C++系列之虚函数技术
输入参数 : None
输出参数 : None
返 回 值 : None
*******************************************************************************/
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class CAnimal
{
public:
virtual void Eat()
{
cout << "CAnimal类,虚函数Eat()" << endl;
}
virtual void Sleep()
{
cout << "CAnimal类,虚函数Sleep()" << endl;
}
};
class CDog : public CAnimal
{
public:
void Sleep()
{
cout << "CDog类继承CAnimal类,重载虚函数Sleep()" << endl;
}
virtual void Sound()
{
cout << "CDog类继承CAnimal类,增加虚函数Sound()" << endl;
}
};
typedef void (*FUNC)(void);
int main(int argc, char *argv[])
{
FUNC pFunc = NULL;
cout << "=======CAnimal Begin=======" << endl;
CAnimal aAnimal;
//aAnimal.Eat();
//aAnimal.Sleep();
cout << "aAnimal地址 = " << &aAnimal << " , sizeof(CAnimal) = " << sizeof(CAnimal) << endl;
cout << "aAnimal虚函数表地址 = " << (int*)&aAnimal << endl;
cout << "aAnimal第一个虚函数 = " << (int*)*(int*)&aAnimal << " , 指向 = " << (int*)*((int*)*(int*)&aAnimal) << endl;
cout << "aAnimal第二个虚函数 = " << (int*)*(int*)&aAnimal + 1 << " , 指向 = " << (int*)*((int*)*(int*)&aAnimal + 1) << endl;
cout << "aAnimal虚函数表结尾 = " << (int*)*(int*)&aAnimal + 2 << " , 指向 = " << (int*)*((int*)*(int*)&aAnimal + 2) << endl;
pFunc = (FUNC) * (int *)*(int *)&aAnimal;
pFunc();
pFunc = (FUNC) * ((int *)*(int *)&aAnimal + 1);
pFunc();
cout << "=======CAnimal End=======" << endl;
cout << endl;
cout << "=======CDog Begin=======" << endl;
CDog aDog;
//aDog.Eat();
//aDog.Sleep();
//aDog.Sound();
cout << "aDog地址 = " << &aDog << " , sizeof(aDog) = " << sizeof(aDog) << endl;
cout << "aDog虚函数表地址 = " << (int*)&aDog << endl;
cout << "aDog第一个虚函数 = " << (int*)*(int*)&aDog << " , 指向 = " << (int*)*((int*)*(int*)&aDog) << endl;
cout << "aDog第二个虚函数 = " << (int*)*(int*)&aDog + 1 << " , 指向 = " << (int*)*((int*)*(int*)&aDog + 1) << endl;
cout << "aDog第三个虚函数 = " << (int*)*(int*)&aDog + 2 << " , 指向 = " << (int*)*((int*)*(int*)&aDog + 2) << endl;
cout << "aDog虚函数表结尾 = " << (int*)*(int*)&aDog + 3 << " , 指向 = " << (int*)*((int*)*(int*)&aDog + 3) << endl;
pFunc = (FUNC) * (int *)*(int *)&aDog;
pFunc();
pFunc = (FUNC) * ((int *)*(int *)&aDog + 1);
pFunc();
pFunc = (FUNC) * ((int *)*(int *)&aDog + 2);
pFunc();
cout << "=======CDog End=======" << endl;
return 0;
}
执行结果:
此处 aAnimal虚函数表结尾 = 0x40b7d0 , 指向 = 0x3a434347 ,最后的指向为啥不是0 ???
对于main函数中的那些指针操作,我们以CAnimal的第二个虚函数Sleep为例解释一下:
(int *)*(int *)&aAnimal + 1
先从aAnimal开始,&aAnimal获取aAnimal对象的地址,第一个int *表示强制转换为int型指针,再往外一个*表示从aAnimal对象首地址取4个字节存储值(32位机器,int占用4个字节),这里就是CAnimal类虚函数表的首地址了,再外层的int *表示将这4个字节解释为一个int型指针,然后后面的+1表示往后面移动一个int型指针长度,即虚函数表第二个表项也就是第二个虚函数的地址。
CDog没有重载Eat虚函数,所以其虚函数和CAnimal指向同一个指令地址,CDog的Sleep虚函数和CAnimal的Sleep虚函数指向不同地址。
1. C++多态是通过是虚函数表实现的;
2. 每个类共享同一个虚函数表;
3. 虚函数表是按照类中声明虚函数顺序依次填充的,结尾为NULL;
4. 子类重载父类虚函数会覆盖父类虚函数在子类虚函数表中相应表项。
/*这里的一点争议的看法*/
原文认为(int*)(&b)是虚表的地址,而很多网友都说,:(int *)*(int*)(&b)才是虚表地址.而(int*)*((int*)*(int*)(&b)); 才是虚表第一个虚函数的地址。其实看后面的调用pFun = (Fun)*((int*)*(int*)(&b)); 就可以看出,*((int*)*(int*)(&b));转成函数指针给pFun,然后正确的调用到了虚函数virtual void f()。
一些关于虚函数面试,以及论坛精彩彩蛋请看博主 https://blog.csdn.net/sunshinewave/article/details/51079204