【摘要】
很多教材上都有介绍到虚指针、虚函数与虚函数表,有的说类对象共享一个虚函数表,有的说,一个类对象拥有一个虚函数表;还有的说,无论用户声明了多少个类对象,但是,这个VTABLE虚函数表只有一个;也有的在说,每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。今天,我们就来解决这个问题,同一个类的不同对象,是不是拥有“相同”的虚函数表,这个相同是物理上的相同(内存地址)还是逻辑上的相同(数据结构)。本文现详述如下!
【正文】
虚指针:每个含有虚方法(虚函数)对象里有虚表指针,指向虚表。
虚函数表:虚函数表是顺序存放虚函数地址的,虚表是顺序表,表里存放了虚函数的地址。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
【代码示例】
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
void h() { cout << "Base::h" << endl; }
};
typedef void(*Fun)(void); //函数指针
int main()
{
Base b;
Base c;
// 这里指针操作比较混乱,在此稍微解析下:
// *****printf("虚表地址:%p\n", *(int *)&b); 解析*****:
// 1.&b代表对象b的起始地址
// 2.(int *)&b 强转成int *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针
// 3.*(int *)&b 取前四个字节,即vptr虚表地址
//
// *****printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);*****:
// 根据上面的解析我们知道*(int *)&b是vptr,即虚表指针.并且虚表是存放虚函数指针的
// 所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(int *)*(int *)&b
// 这样强转后为了后面的取四个字节.所以*(int *)*(int *)&b就是虚表的第一个元素.
// 即f()的地址.
// 那么接下来的取第二个虚函数地址也就依次类推. 始终记着vptr指向的是一块内存,
// 这块内存存放着虚函数地址,这块内存就是我们所说的虚表.
//
printf("虚表地址:%p\n", *(int*)(&b));
printf("第一个虚函数地址:%p\n", * (int*)*(int*)(&b) );
printf("第二个虚函数地址:%p\n", *((int*)*(int*)(&b)+2));
Fun pfun = (Fun)*((int*)*(int*)(&b)); //vitural f();
printf("f():%p\n", pfun);
pfun();
pfun = (Fun)(*((int*)*(int*)(&b) + 2)); //vitural g();
printf("g():%p\n", pfun);
pfun();
printf("虚表地址:%p\n", *(int*)(&c));
printf("第一个虚函数地址:%p\n", * (int*)*(int*)(&c));
printf("第二个虚函数地址:%p\n", *((int*)*(int*)(&c)+2));
pfun = (Fun)*((int*)*(int*)(&c)); //vitural f();
printf("f():%p\n", pfun);
pfun();
pfun = (Fun)(*((int*)*(int*)(&c) + 2)); //vitural g();
printf("g():%p\n", pfun);
pfun();
}
代码是在Ubuntu16.04 64bit下跑的,在32bit下程序要把2修改成1,这个是由于32bit和64bit的区别。
另外本代码在win10+VS2015下没法跑
如果Base访问数据成员,而我们通过获取函数指针来访问函数,这个会出现SegmentFault 错误,因为我们通过强制转换的函数指针访问数据成员的时候,函数是不找到this指针的,所以会报错,这个建议和this指针到底存放到哪里一起学习。
【结论】
不同对象虚函数表表中元素是相等的,逻辑上是一样的,存放的都是类中虚函数的地址;
不同对象虚函数表的内存地址是一样的,也即所有对象公共一个表
这个和获取C++虚表地址和虚函数地址 的做法是一样的,
另外这个C++ 虚函数表解析 是存在问题的,关于取虚函数表的地址,它是按照对象的地址,也即this指针的地址做得,这个是不对的。