C++ 对象内存布局 (4)
注意:關于內存對齊(memory alignment),請看關于內存對齊問題,后面將會用到。
?
下面我們進行在普通繼承(即非虛繼承)時,派生類的指針轉換到基類指針的情形研究。假定各類之間的關系如下圖:
?
代碼如下:
#include <iostream>
using namespace std;
?
#pragma vtordisp(off)
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 = 0x00373F08
???????? Parent* pp = (Parent*)pc;?????????? ->????? ???????? pp = 0x00373F08
還有上面的2和4的輸出也是一樣的:
???????? GrandChild* pgc = new GrandChild();?????? ->????????? pgc = 0x00373F40
???????? Child* pc2 = (Child*)pgc;????????????????????????????? -> ???????? pc2 = 0x00373F40
?
保持整個程序其他部分代碼不做任何變動,我們將Child改為從Parent虛繼承,改后的Child代碼如下:
class Child : public virtual Parent
{
public:
???? int child;
};
運行結果為下圖值下半部分:
?
?
現在來一一比較兩者之間的不同:
第1條,兩者相同;
第2條,兩者相同;
第3條,由0x00373F08變成了0x00373F10了,也就是說經過Parent* pp = (Parent*)pc;后,即由pc = 0x00373F08得到了pp = 0x00373F10。很奇怪!
第4條,兩者相同;
第5條,由0x00373F40變成了0x00373F4C了,也就是說經過Parent* pp2 = (Parent*)pc2;后,即由pc2 = 0x00373F40得到了pp2 = 0x00373F4C。很奇怪!
第6條,由0x00373F40變成了0x00373F4C了,也就是說經過Parent* pp3 = (Parent*)pgc;后,即由pgc = 0x00373F40得到了pp3 = 0x00373F4C。很奇怪!
?
為什么會這樣呢?通過上述分析發現,出現這種指針發生變化的情況,均發生在將Child*或者GrandChild*轉換到Parent*的各行。Parent是Child的虛基類,Child又是GrandChild的基類。在上面的第4條中,我們通過Child* pc2 = (Child*)pgc;,試圖將GrandChild*轉換為Child*,事實上也轉換成功了,同時指針的值并沒有發生改變。GrandChild是普通繼承于Child的,而非虛擬繼承,換言之,Child不是GrandChild的虛基類,所以指針轉換時,目標指針的值和賦給它的值保持一致。通過這樣的分析我們似乎可以得出下面的結論:
當一個派生類對象的指針轉換到虛基類指針時(不管兩者之間是否有其他中間類,而且也不管這些中間類是否是派生類的普通基類還是虛基類),指針的值就會發生變化。
?
為了驗證上述結論,在上面的基礎上,我們將GrandChild改為虛擬繼承Child,修改后的GrandChild代碼如下:
class GrandChild : public virtual Child
{
public:
???????? int grandchild;
};
運行程序,得到如下結果(下圖的最下面一部分):
?
?
我們看到,現在第4條也發生了變化。因此原來的結論是成立的。再次總結一下這條非常重要的結論:
如果沒有虛擬繼承,當將派生類對象的指針轉換到基類時(即使基類中有虛函數),指針的值不會發生變化;但當一個派生類對象的指針轉換到虛基類指針時(不管兩者之間是否有其他中間類,而且也不管這些中間類是否是派生類的普通基類還是虛基類),指針的值就會發生變化。
?
這個結論對后面的了解含有虛基類的對象內存布局有著非同一般的意義。對于這個結論,我們還剩下一個問題,那就是為什么會這樣呢??前面我們可以看到賦值后的指針的值并不等于賦給它的對象地址值。也就是說在這個賦值過程中編譯器進行了額外的工作,即調整了指針的值。我們看看上面程序中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行代碼,它們通過偏移值指針找到偏移值,并以此來調整指針的位置,讓目的指針最終指向對象中的基類部分的數據成員。
?
至此,我們解釋清楚了上面的問題。因為這部分討論的結果太重要了,我們不妨再次總結如下:
如果沒有虛擬繼承,當將派生類對象的指針轉換到基類時(即使基類中有虛函數),指針的值不會發生變化;但當一個派生類對象的指針轉換到虛基類指針時(不管兩者之間是否有其他中間類,而且也不管這些中間類是否是派生類的普通基類還是虛基類),指針(目的指針)的值就會發生變化,目的指針最終指向對象中的基類部分(或曰基類的實例)。
轉載于:https://www.cnblogs.com/linqiang/p/3391319.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的C++ 对象内存布局 (4)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据的封装
- 下一篇: 【转载】C++操作符