跟我一起学习C++虚函数--第一篇
我们知道,虚函数作为C++实现多态的方式,具有强大的RTTI(RunTime Type Identification)功能。虚函数使用起来比较简单,但是也很容易出错。本系列将带着你一步一步了解虚函数的内部实现机制,在掌握原理后,我相信你会对虚函数以及C++本身会有进一步的认识和理解。
注:本系列所有的关于C++虚函数的探索都是在GCC平台上进行的。
一、带有虚函数的对象内存布局
让我们先看一段代码:
View Code
1 #include <iostream> 2 using namespace std; 3 4 class Test 5 { 6 public: 7 virtual void f(){cout << "Func:f()" << endl;}; 8 9 public: 10 int a; 11 }; 12 13 int main() 14 { 15 cout << sizeof(Test) << endl; //输出:8 16 Test test; 17 cout << &test << "\t" << &test.a << endl;//输出:0xbf91e508 0xbf91e50c 18 return 0; 19 }
在上面的例子中,我们定义了一个简单的包含一个虚函数的类。从输出我们可以看出两点:
1)编译器在包含虚函数的类中插入了虚指针vptr,大小为4。
2)对包含虚函数的对象,虚指针被安放在对象的起始位置。如下图所示:
这个例子我们只是简单地介绍了包含虚函数对象的成员内存布局,下面我们慢慢深入看看虚表的结构。还是从例子入手:
View Code
1 #include <iostream> 2 using namespace std; 3 4 class Test 5 { 6 public: 7 virtual void f(){cout << "Func:f()" << endl;}; 8 virtual void g(){cout << "Func:g()" << endl;}; 9 virtual void h(){cout << "Func:h()" << endl;}; 10 private: 11 int a; 12 }; 13 14 int main() 15 { 16 Test test; 17 typedef void (*Func)(void);//定义个函数指针宏 18 cout << "vptbl address:" << (int *)(&test) << endl;//输出:vptbl address:0xbfc85494 19 cout << "func1 address:" << (int *)*(int *)(&test) << endl;//输出:func1 address:0x8048988 20 21 Func pFunc = (Func)*((int *)*(int *)(&test)); 22 pFunc();//输出:Func:f() 23 24 pFunc = (Func)*((int *)*(int *)(&test)+1); 25 pFunc();//输出:Func:g() 26 27 pFunc = (Func)*((int *)*(int *)(&test)+2); 28 pFunc();//输出:Func:h() 29 30 return 0; 31 }
从输出结果我们可以看出,我们可以直接获取到虚函数表以及表中每一个函数的地址。如下图所示:
这里对上面的操作进行简单解释,主要是指针方面的:
1)取得虚函数表地址:(int *)(&test)。
在前面我们已经讨论过,对象的起始地址为虚指针地址。对对象地址进行int *强制转换实际上就是得到虚指针,而虚指针的内容就是虚函数表地址。
2)取得虚函数表中第一个虚函数地址:(int *)*(int *)(&test)。
取得虚表地址后,再次进行取址便得到了第一个虚函数地址。
3)调用虚函数表中的函数(Func)*((int *)*(int *)(&test))。
取得第一个虚函数地址后,进行强制类型转换,便得到了函数地址。
好,到这里相信大家应该对带有虚函数的对象的内存布局有一个较为清楚的了解了吧。下一章我们讨论在继承情况下的内存布局情况。
参考文献:
1.《深度探索C++对象模型》