对C++虚函数的理解
以前只知道虚函数是由一个叫作“虚函数表”的东西实现,但是一直不知道具体是怎么实现,今天看到****学院上c++虚函数的原理时,有几个问题,还是不怎么清楚,只好做一些测试,具体让我产生疑问的截图如下:
从上面的截图里,主要有2个问题
1.按上图所说,编译器会往类中插入“指向虚函数表的指针(__vfptr)”,那么如果这个类所生成几个不同的对象,这几个对象的__vfptr都相同吗?
2.子类和父类是共用一个虚函数表吗?
关于这两个问题,我写了一个程序来验证
#include "stdio.h"
class Animal
{
public:
virtual void eat()
{
printf("animal eat\n");
}
virtual void crazy()
{
printf("animal crazy\n");
}
};
class Cat :public Animal
{
};
class WhiteCat :public Cat
{
public:
void crazy()
{
printf("WhiteCat crazy\n");
}
};
void main()
{
Animal animal1;
Animal animal2;
printf("animal1 __vfptr:%X\n",animal1);
printf("animal2 __vfptr:%X\n", animal2);
Cat cat1;
Cat cat2;
printf("cat1 __vfptr:%X\n",cat1 );
printf("cat2 __vfptr:%X\n",cat2);
WhiteCat whiteCat1;
WhiteCat whiteCat2;
printf("whiteCat1 __vfptr:%X\n", whiteCat1);
printf("whiteCat1 __vfptr:%X\n", whiteCat2);
getchar();
}
在getchar()函数下一个断点启动调试,此时程序运行如下
1可以看见animal1和animal2的__vfptr是一样的,同样cat1和cat2,whitecat1和whitecat2也是如此,但是cat和animal的__vfptr却并不相同
分析:(1)animal1和animal2都是Animal类生成的实例,他们指向了同一个虚函数表,同理cat1和cat2,whitecat1和whitecat2也是如此
(2)尽管Cat是从Animal派生,并且Cat也没有改写基类的虚函数,但是该类生成的实例所对应的__vfptr却与基类不一样,这也就意味着基类和派生类并没有共用一个虚函数表
下面继续分析,虚函数表是怎么实现的,
(1)在vs里,点击右键---转到反汇编
(2)在汇编界面地址栏里输入0xDB7B34(animal1和animal2的__vfptr指向的地址),并回车
(3)我们可以看到animal1和animal2的__vfptr指向的地址存储了7C 11 DB 00 4F 11 DB 00 00 00 00 00 ...等字节,我们知道__vfptr是指向虚函数表的,而虚函数表可以看成是函数指针数组(存储函数指针的数组),我们先取前4个字节看看到底是不是指向函数指针,前4个字节是7C 11 DB 00,因为x86结构是属于“小端”架构(低字节在低位,高字节在高位),所以这4个字节对应的函数指针是00DB117C,此时我们在汇编界面的地址栏输入0xDB117C,并回车,如下图所示
(4)很明显0xDB117C存储的是一段汇编代码 jmp Animal::eat,了解汇编代码的人都知道,该指令是无条件跳转指令,直接跳转到Animal::eat的汇编代码段
用同样的方法测试下4个字节4F 11 DB 00
接下来的 00 00 00 00 这4个字节,是虚函数表的结尾标志。
大家也可以用同样的方法查看派生类的虚函数表。。。
至此,我们终于得见虚函数表的真实面目啦,也许有人会问这个虚函数表好像跟想象的不一样,虚函数表不是存储虚函数地址的吗,这里怎么会是jmp指令,虽然最终可以通过这个jmp指令转到虚函数地址,但是他终究不是虚函数地址呀,难道书上面说错了吗?(答:书上没有说错,因为这是debug调试版本,在debug调试版本里是会出现jmp指令的,但release版本就不是jmp指令了,具体debug版本为什么要有jmp,为什么要多此一举,就下次再说吧)