在C++中,指針的類型轉換是經常發生的事情,比如將派生類指針轉換為基類指針,將基類指針轉換為派生類指針。指針的本質其實就是一個整數,用以記錄進程虛擬內存空間中的地址編號,而指針的類型決定了編譯器對其指向的內存空間的解釋方式。
基於上面的理解,我們似乎可以得出一個結論,C++中對指針進行類型轉換,不會改變指針的值,只會改變指針的類型(即改變編譯器對該指針指向內存的解釋方式),但是這個結論在C++多重繼承下是 不成立的。
看下面一段代碼:
1 #include <iostream> 2 using namespace std; 3 4 class CBaseA 5 { 6 public: 7 char m_A[32]; 8 }; 9 10 class CBaseB 11 { 12 public: 13 char m_B[64]; 14 }; 15 16 class CDerive : public CBaseA, public CBaseB 17 { 18 public: 19 char m_D[128]; 20 }; 21 22 23 int main() 24 { 25 auto pD = new CDerive; 26 auto pA = (CBaseA *)pD; 27 auto pB = (CBaseB *)pD; 28 29 cout << pA << '\n' << pB << '\n' << pD << endl; 30 cout << (pD == pB) << endl; 31 }
這段代碼的輸出是:
0x9f1080
0x9f10a0
0x9f1080
1
可以看出,指向同一個堆上new出來的內存指針,在經過類型轉換之后,其值會發生改變。究其原因,要從C++中多重繼承的內存布局說起。
new CDerive;執行之后,生成的內存布局如下:

同時我們注意到,pB與pD的指針差值正好是CBaseA占用的內存大小32字節,而pA與pD都指向了同一段地址。這是因為,將一個派生類的指針轉換成某一個基類指針,編譯器會將指針的值偏移到該基類在對象內存中的起始位置。
可是為什么C++要這樣設計呢?
試想,沿用上面的場景,如果pB和pA都指向對象的首地址,那么使用pB指針來定位CBaseB的成員變量m_B時,編譯器應該將pB指針偏移32個字節,從而跳過CBaseA的內存部分。而pB指針如果是這樣產生的auto pB = new CBaseB;,那么使用pB指針來定位CBaseB的成員變量m_B時,偏移量應該為0。
關鍵在於對於一個指針而言,編譯器不關心也無法知道該指針的來源(一種極端情況,指針是從其他模塊傳遞過來的),而只是把它視為一個有指針類型的整數。所以對於CBaseB類型的指針,取CBaseB的成員變量m_B時,偏移量應該通通為0,這是通過CBaseB的類聲明就可以統一決策的事情。
說到這里,pD和pB的指針地址為什么不一樣大家應該清楚了,可是為什么下面的代碼輸出是1呢?
cout << (pD == pB) << endl;
輸出1表示pD和pB是相等的,而剛剛我們才說明了,pD和pB的地址是相差了32個字節的。
其實這也是編譯器為大家屏蔽了這種指針的差異,當編譯器發現一個指向派生類的指針和指向其某個基類的指針進行==運算時,會自動將指針做隱式類型提升已屏蔽多重繼承帶來的指針差異。因為兩個指針做比較,目的通常是判斷兩個指針是否指向了同一個內存對象實例,在上面的場景中,pD和pB雖然指針值不等,但是他們確確實實都指向了同一個內存對象(即new CDerive;產生的內存對象 ),所以編譯器又在此處插了一腳,讓我們可以安享==運算的上層語義。
