前言
看了陈皓大佬的文章《C++ 虚函数表解析》后,在自己理解上出现一些问题,验证后明了写出此文章。前提不是说大佬写错了,而是我看明白了。
直接写出问题结论:C++的虚函数表是类对象共享的,而类实例的虚函数表存放的是唯一虚函数的地址(即这是地址表),不存在每一个类实例都建立虚函数
下面我们开始验证过程。
虚函数表
学习C++的人都知道类实例建立时的栈内存分布最前面位置是一个虚函数表指针(vptr),其中存放了类的虚函数地址表的地址,这张表解决了继承、覆盖等问题,主要实现了多态的机制。而虚表指针在内对象的首地址的原因是:
- 为了保证虚函数表有最高的性能
- 虚函数表是类对象共享的,每个类对象都用同一个虚函数表
编译器处理虚函数的方法
给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了为类对象进行声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中,注意虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。
下面开始验证过程(Windows10+VS2013 x86)
1.获取类虚函数表地址
首先我们写一个带有虚函数的类
class Base
{
public:
virtual void f()
{
cout << "Base:f" << endl;
}
virtual void g()
{
cout << "Base:g" << endl;
}
virtual void h()
{
cout << "Base:h" << endl;
}
int a = 100;
};
然后通过以下代码来获取vptr、虚函数表的地址和各虚函数的地址
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表指针地址:" << (int*)(&b) << endl;
cout << "b.a地址:" << (int*)((int*)(&b)+1)<< endl;
cout << "b.a的地址:" << (int*)&(b.a)<< endl;
cout << "虚函数表首地址:" << (int*)*(int*)(&b) << endl;
cout << "虚函数表-第一个函数地址:" << (int*)*(int*)*(int*)(&b) << endl;
cout << "虚函数表-第二个函数地址:" << (int*)*((int*)*(int*)(&b) + 1) << endl;
cout << "虚函数表-第三个函数地址:" << (int*)*((int*)*(int*)(&b) + 2) << endl;
运行结果
而使用VS Debug下可以监视可以得到:
结果与我们的结果一致。
我们对(int*)*(int*)*(int*)(&b)进行分析一下:我们对&b强转为int*得到了b的内存首地址,然后(int*)*是对此地址取值再强转得到虚函数表的首地址,后面再一次取值强转就得到了一个虚函数的地址指针。
图表示如下:
2.无虚函数覆盖下的继承虚函数表
在上例的基础上如果我们添加一个子类继承,此子类没有虚函数覆盖,那么它的虚函数表是怎样的呢?
首先添加子类
class Device :public Base
{
virtual void f1()
{
cout << "Device:f1" << endl;
}
virtual void g1()
{
cout << "Device:g1" << endl;
}
virtual void h1()
{
cout << "Device:h1" << endl;
}
};
测试代码:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表指针地址:" << (int*)(&b) << endl;
cout << "b.a地址:" << (int*)((int*)(&b)+1)<< endl;
cout << "b.a的地址:" << (int*)&(b.a)<< endl;
cout << "虚函数表首地址:" << (int*)*(int*)(&b) << endl;
cout << "虚函数表-第一个函数地址:" << (int*)*(int*)*(int*)(&b) << endl;
cout << "虚函数表-第二个函数地址:" << (int*)*((int*)*(int*)(&b) + 1) << endl;
cout << "虚函数表-第三个函数地址:" << (int*)*((int*)*(int*)(&b) + 2) << endl;
pFun = (Fun)(int*)*(int*)*(int*)(&b);
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&b) + 1);
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&b) + 2);
pFun();
Device d;
cout << "Device的虚函数表地址:" << (int*)(&d) << endl;
cout << "d.a地址:" << (int*)((int*)(&d) + 1) << endl;
cout << "d.a的地址:" << (int*)&(d.a) << endl;
cout << "虚函数表首地址:" << (int*)*(int*)(&b) << endl;
cout << "Device的虚函数表-第一个函数地址:" << (int*)*(int*)*(int*)(&d) << endl;
cout << "Device的虚函数表-第二个函数地址:" << (int*)*((int*)*(int*)(&d) + 1) << endl;
cout << "Device的虚函数表-第三个函数地址:" << (int*)*((int*)*(int*)(&d) + 2) << endl;
cout << "Device的虚函数表-第四个函数地址:" << (int*)*((int*)*(int*)(&d) + 3) << endl;
cout << "Device的虚函数表-第五个函数地址:" << (int*)*((int*)*(int*)(&d) + 4) << endl;
cout << "Device的虚函数表-第六个函数地址:" << (int*)*((int*)*(int*)(&d) + 5) << endl;
pFun = (Fun)(int*)*((int*)*(int*)(&d));
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&d) + 1);
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&d) + 2);
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&d) + 3);
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&d) + 4);
pFun();
pFun = (Fun)(int*)*((int*)*(int*)(&d) + 5);
pFun();
运行结果:
可以看到子类创建了自己的虚函数表,虚函数表中先存储的是父类的虚函数表,后面才是自己的虚函数表,但是虚函数表中存放的虚函数地址还是Base类对象共享的虚函数地址。
3.有虚函数覆盖下的继承虚函数表
那么如果我在子类覆盖了父类的虚函数呢?
子类对象变为:
class Device :public Base
{
virtual void f()
{
cout << "Device:f" << endl;
}
virtual void g1()
{
cout << "Device:g1" << endl;
}
virtual void h1()
{
cout << "Device:h1" << endl;
}
};
测试代码减少第六个函数地址
运行结果为:
可以看到子类的虚函数首地址是覆盖的虚函数,此虚函数有了自己的新地址,然后是没有覆盖的父类虚函数,地址与父类一致,最后才是自己的虚函数。
关于多重继承我就不再测试了,用的很少
总结
C++编译器为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),每个类使用一个虚函数表,每个类对象用一个虚表指针。
最后给出C++ Primer Plus中的一张图