前篇:http://blog.csdn.net/pathuang68/archive/2009/04/24/4105608.aspx
注意:關於記憶體對齊(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行代碼,它們通過位移值指標找到位移值,並以此來調整指標的位置,讓目的指標最終指向對象中的基類部分的資料成員。
至此,我們解釋清楚了上面的問題。因為這部分討論的結果太重要了,我們不妨再次總結如下:
如果沒有虛擬繼承,當將衍生類別對象的指標轉換到基類時(即使基類中有虛函數),指標的值不會發生變化;但當一個衍生類別對象的指標轉換到虛基類指標時(不管兩者之間是否有其他中間類,而且也不管這些中間類是否是衍生類別的普通基類還是虛基類),指標(目的指標)的值就會發生變化,目的指標最終指向對象中的基類部分(或曰基類的執行個體)。
後篇:http://blog.csdn.net/pathuang68/archive/2009/04/24/4105810.aspx