注意:关于内存对齐(memory alignment),请看关于内存对齐问题,后面将会用到。
下面我们进行在普通继承(即非虚继承)时,派生类的指针转换到基类指针的情形研究。假定各类之间的关系如下图:
代码如下:
#include <iostream> using namespace std; class Parent { public: int parent; }; class Child : public Parent { public: int child; }; class GrandChild : public Child { public: int grandchild; }; int main(void) { Child* pc = new Child(); GrandChild* pgc = new GrandChild(); cout << "1. The address of Child object:\t\t"; cout << (unsigned long*)pc << endl; cout << "2. The address of GrandChild object:\t"; cout << (unsigned long*)pgc << endl; // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent* Parent* pp = (Parent*)pc; cout << "3. Child* casted to Parent*:\t\t"; cout << (unsigned long*)pp << endl; // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child* Child* pc2 = (Child*)pgc; cout << "4. GrandChild* casted to Child*:\t"; cout << (unsigned long*)pc2 << endl; // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent* Parent* pp2 = (Parent*)pc2; cout << "5. Child* casted to Parent*:\t\t"; cout << (unsigned long*)pp2 << endl; // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent* Parent* pp3 = (Parent*)pgc; cout << "6. GrandChild* casted to Parent*:\t"; cout << (unsigned long*)pp3 << endl; return 0; }
运行结果:
我们发现在普通继承的情况下,将派生类对象的指针upcast为基类指针时,指针的值并不会发生改变。
比如上面输出中的1和3是一样的。
Child* pc = new Child(); -> pc = 0x3d10e0
Parent* pp = (Parent*)pc; -> pp = 0x3d10e0
还有上面的2和4的输出也是一样的:
GrandChild* pgc = new GrandChild(); -> pgc = 0x3d10f0
Child* pc2 = (Child*)pgc; -> pc2 = 0x3d10f0
Grandchild的内存分布图:
保持整个程序其他部分代码不做任何变动,我们将Child改为从Parent虚继承,改后的Child代码如下:
#include <iostream> using namespace std; class Parent { public: int parent; }; class Child : public virtual Parent { public: int child; }; class GrandChild : public Child { public: int grandchild; }; int main(void) { Child* pc = new Child(); GrandChild* pgc = new GrandChild(); cout << "1. The address of Child object:\t\t"; cout << (unsigned long*)pc << endl; cout << "2. The address of GrandChild object:\t"; cout << (unsigned long*)pgc << endl; // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent* Parent* pp = (Parent*)pc; cout << "3. Child* casted to Parent*:\t\t"; cout << (unsigned long*)pp << endl; // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child* Child* pc2 = (Child*)pgc; cout << "4. GrandChild* casted to Child*:\t"; cout << (unsigned long*)pc2 << endl; // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent* Parent* pp2 = (Parent*)pc2; cout << "5. Child* casted to Parent*:\t\t"; cout << (unsigned long*)pp2 << endl; // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent* Parent* pp3 = (Parent*)pgc; cout << "6. GrandChild* casted to Parent*:\t"; cout << (unsigned long*)pp3 << endl; return 0; }
运行结果:
与上图相比较
现在来一一比较两者之间的不同:
第1条,两者相同;
第2条,两者相同;
第3条,由0x7d10e0变成了0x7d10e8了,也就是说经过Parent* pp = (Parent*)pc;后,即由pc = 0x7d10e0得到了pp = 0x7d10e8。很奇怪!
第4条,两者相同;
第5条,由0x7d10f8变成了0x7d1104了,也就是说经过Parent* pp2 = (Parent*)pc2;后,即由pc2 = 0x7d10f8得到了pp2 = 0x7d1104。很奇怪!
第6条,由0x7d10f8变成了0x7d1104了,也就是说经过Parent* pp3 = (Parent*)pgc;后,即由pgc = 0x7d10f8得到了pp3 = 0x7d1104。很奇怪!
为什么会这样呢?通过上述分析发现,出现这种指针发生变化的情况,均发生在将Child*或者GrandChild*转换到Parent*的各行。Parent是Child的虚基类,Child又是GrandChild的基类。在上面的第4条中,我们通过Child* pc2 = (Child*)pgc;,试图将GrandChild*转换为Child*,事实上也转换成功了,同时指针的值并没有发生改变。GrandChild是普通继承于Child的,而非虚拟继承,换言之,Child不是GrandChild的虚基类,所以指针转换时,目标指针的值和赋给它的值保持一致。通过这样的分析我们似乎可以得出下面的结论:
当一个派生类对象的指针转换到虚基类指针时(不管两者之间是否有其他中间类,而且也不管这些中间类是否是派生类的普通基类还是虚基类),指针的值就会发生变化。
Child和Grandchild的内存分布图:
为了验证上述结论,在上面的基础上,我们将GrandChild改为虚拟继承Child,修改后的GrandChild代码如下:
#include <iostream> using namespace std; class Parent { public: int parent; }; class Child : public virtual Parent { public: int child; }; class GrandChild : public virtual Child { public: int grandchild; }; int main(void) { Child* pc = new Child(); GrandChild* pgc = new GrandChild(); cout << "1. The address of Child object:\t\t"; cout << (unsigned long*)pc << endl; cout << "2. The address of GrandChild object:\t"; cout << (unsigned long*)pgc << endl; // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent* Parent* pp = (Parent*)pc; cout << "3. Child* casted to Parent*:\t\t"; cout << (unsigned long*)pp << endl; // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child* Child* pc2 = (Child*)pgc; cout << "4. GrandChild* casted to Child*:\t"; cout << (unsigned long*)pc2 << endl; // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent* Parent* pp2 = (Parent*)pc2; cout << "5. Child* casted to Parent*:\t\t"; cout << (unsigned long*)pp2 << endl; // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent* Parent* pp3 = (Parent*)pgc; cout << "6. GrandChild* casted to Parent*:\t"; cout << (unsigned long*)pp3 << endl; return 0; }
运行程序,得到如下结果:
与上面两个图相比较:
我们看到,现在第4条也发生了变化。因此原来的结论是成立的。再次总结一下这条非常重要的结论:
如果没有虚拟继承,当将派生类对象的指针转换到基类时(即使基类中有虚函数),指针的值不会发生变化;但当一个派生类对象的指针转换到虚基类指针时(不管两者之间是否有其他中间类,而且也不管这些中间类是否是派生类的普通基类还是虚基类),指针的值就会发生变化。
Grandchild的内存分布图:
这个结论对后面的了解含有虚基类的对象内存布局有着非同一般的意义。对于这个结论,我们还剩下一个问题,那就是为什么会这样呢? 前面我们可以看到赋值后的指针的值并不等于赋给它的对象地址值。也就是说在这个赋值过程中编译器进行了额外的工作,即调整了指针的值。我们看看上面程序中Parent* pp = (Parent*)pc; (向上类型转换,即up-casting) 这行对应的汇编代码(在VC中,进行debug时,按Alt 8,即可查看到汇编代码),看看编译器究竟做了些什么?
38: Parent* pp = (Parent*)pc; 00401691 cmp dword ptr [ebp-10h],0 00401695 jne main+120h (004016a0) 00401697 mov dword ptr [ebp-40h],0 0040169E jmp main+12Eh (004016ae) 004016A0 mov eax,dword ptr [ebp-10h] 004016A3 mov ecx,dword ptr [eax] // 6 004016A5 mov edx,dword ptr [ebp-10h] // 7 004016A8 add edx,dword ptr [ecx+4] // 8 004016AB mov dword ptr [ebp-40h],edx 004016AE mov eax,dword ptr [ebp-40h] 004016B1 mov dword ptr [ebp-18h],eax
重要的是第6、7、8行代码,它们通过偏移值指针找到偏移值,并以此来调整指针的位置,让目的指针最终指向对象中的基类部分的数据成员。
至此,我们解释清楚了上面的问题。因为这部分讨论的结果太重要了,我们不妨再次总结如下:
如果没有虚拟继承,当将派生类对象的指针转换到基类时(即使基类中有虚函数),指针的值不会发生变化;但当一个派生类对象的指针转换到虚基类指针时(不管两者之间是否有其他中间类,而且也不管这些中间类是否是派生类的普通基类还是虚基类),指针(目的指针)的值就会发生变化,目的指针最终指向对象中的基类部分(或曰基类的实例)。