(VC6实现的C++对象模型) 在C++对象模型中,类成员的分布情况: 1) non-static 数据成员位于每个对象之中 2) static 数据成员与所有的函数都位于对象之外 处理虚函数: 类的所有虚函数地址放在一块,称之为虚拟函数表。 编译器为每个类对象插入一指针vptr 指向该虚函数表。 虚函数在函数表中的位置按一定的顺序,每个函数名对应着一个slot_id,调用的时候直接调用v-table的slot_id对应的函数。 上面是最基本的模型。至于virtual-base 多重继承 等的模型也是基于上述模型进行一些改变,具体等到后面再说~,先把简单的搞定了J 不考虑虚函数的数据布局
struct A { void v_fun1(){}; void v_fun2(){}; int a; int b; int c; static int d; }; main () { A a,b; cout<<FMT<<"sizeof(a) = "<<sizeof(a)<<endl; cout<<FMT<<"&a = "<<&a<<endl; cout<<FMT<<"&a.a = "<<&a.a<<endl; cout<<FMT<<"&a.b = "<<&a.b<<endl; cout<<FMT<<"&a.c = "<<&a.c<<endl; cout<<FMT<<"&b = "<<&b<<endl; cout<<FMT<<"&b.a = "<<&b.a<<endl; cout<<FMT<<"&b.b = "<<&b.b<<endl; cout<<FMT<<"&b.c = "<<&b.c<<endl; cout<<"/t——————————"<<endl; cout<<FMT<<"&a.d = "<<&a.d<<endl; cout<<FMT<<"&b.d = "<<&b.d<<endl; printf("/t&a.fun1 = %p/n",a.fun1); printf("/t&a.fun2 = %p/n",a.fun2); printf("/t&b.fun1 = %p/n",b.fun1); printf("/t&b.fun2 = %p/n",b.fun2); cout<<endl;} // [输出结果] sizeof(a) = 12 &a = 0012FF74 &a.a = 0012FF74 &a.b = 0012FF78 &a.c = 0012FF
7C &b = 0012FF68 &b.a = 0012FF68 &b.b = 0012FF
6C &b.c = 0012FF70 ————————— &a.d =
0047873C &b.d =
0047873C &a.fun1 = 00401235 &a.fun2 = 00401181 &b.fun1 = 00401235 &b.fun2 = 00401181 |
对象的大小正好是3个non-static变量的大小之和, a,b的地址之后,紧接着的是各自的数据成员的地址:”non-static 成员变量分别存在于对象中”, a和b获得的static数据成员和函数地址(事实上并不是实际的函数地址)都是一样的,因此他们的位置与具体对象无关:”成员函数以及static型数据成员则放置于对象之外,对象a,b共享成员函数以及静态成员变量” J。可以用下面的图显示:
图 1 由于数据成员的内存分布连续,那么我们很理所当然的写出下面的代码……
class A { A() { memset(this,0,sizeof(A)); // [取代下面的初始化] // a = 0; // b = 0; // c= 0; }; }; |
现在这样写是正确的,不过如果加入虚函数,问题就来了…… 具有虚函数对象数据分布
struct A { // [加上virtual] virtual void v_fun1(){cout<<"v_fun1"<<endl;}; virtual void v_fun2(){cout<<"v_fun2"<<endl;}; int a; int b; int c; }; main() { A a,b; cout<<FMT<<"sizeof(a) = "<<sizeof(a)<<endl; cout<<FMT<<"&a = "<<&a<<endl; cout<<FMT<<"&a.a = "<<&a.a<<endl; cout<<FMT<<"&a.b = "<<&a.b<<endl; cout<<FMT<<"&a.c = "<<&a.c<<endl; cout<<endl; } // [输出结果] sizeof(a) = 16 &a = 0012FF70 &a.a = 0012FF74 &a.b = 0012FF78 &a.c = 0012FF
7C |
给A的函数加上virtual 之后,再看看结果的不同: 1) 对象a 的大小增大了4字节 2) 对象a的地址和a的第一个成员地址之间有4个字节的’不明数据’。 “不明数据”其实就是我们一开始提到过的vptr,编译器私下为对象加入的指针,指向v-table。而v-table中储存着类A的所有虚函数地址 v_fun1, v_fun2。
具体布局如下图所示:
图 2
为什么要加入这样的机制呢,或者说为什么选择了这样的模型呢?
主要考虑的还是效率的问题……,For further info,please consult The c++ object model 哈~J
再通过代码来验证一下~J~
验证思路:
如果存在vptr ,那么他保存的数据就是vitrual-table 的地址,那么,所有的虚函数的真实地址应该都在这里了,知道了地址后, 如何调用呢? 1. 使用正常的成员函数调用方式 (obj.*fun)( parm )
2. 非成员函数的方式调用,既然知道的是真实地址,那么如果再知道函数的原形就解决了,记得this 指针吧,(曾几何时,因为它还莫名其妙了一阵子,哈~),在VC6中,他是做为函数的第一个参数传入的,其他参数以成员函数的一样。
|
^_^ 就按照这样的思路,把找到的地址直接调用,哈~,希望别找出乱七八糟的地址(大不了程序崩溃)!@$%%&(
// [获得vptr指针本身的地址就是&a] int *addr_vptr = (int*)&a; // [获得vptr指向的地址(v-table 的首地址)] int *vptr = (int*)((*addr_vptr)); // [获得第一个函数的地址] int *p_v_fun = (int*)((*vptr));
// [现在函数地址已经找到了,调用一下看会不会程序崩溃 J~~~!!] // [记得this指针吧,成员函数经过编译器的捣乱后的函数] // [——把this做为参数传到成员函数中……] typedef void(*PF)(A*); PF pf = (PF)( p_v_fun ); (*pf)(&a);
// [输出结果] v_fun1 |
哇,还真有那么回事,不会是碰巧吧——再来调用一下第二个虚拟函数。
// [virtual table中第2个存储单元地址] vptr = (int*)((*addr_vptr)+4); // [获得第二个函数的地址] p_v_fun = (int*)((*vptr)); pf = (PF)( p_v_fun ); (*pf)(&a);
// [输出结果] v_fun2 |
恩,的确是那么回事,HOHO。不信自己试试 ~J~ 继续…… memset 的使用导致错误。 继续之前,再回顾一下前面分析的最后一段代码。
class A { A() { memset(this,0,sizeof(A)); // [取代下面的初始化] // a = 0; // b = 0; // c= 0; }; }; |
现在再看它,应该就能发现问题了! 记得constructor semantic 中说过,编译器初始化vptr的操作会放在constructor 的user-code 前,~ 类似下面的代码
// [Initialize vptr ……] …… // [user code] memset(this,0,sizeof(A)); |
终于看到狐狸尾巴了J。编译器初始化完vptr后,我们的user-code 又把vptr 给刷了~, 编译器白忙了一场……L
继续刚才的话题
上面虽然有了虚函数,但还没考虑到继承,现在分析一下drive-class中的数据分布,他是如何同base-class 联系起来的…… 差点忘记今天是国庆节,嘿,休息休息~ 上网看看MM J
|