面向對象的三大特性之一就是繼承,繼承運行我麼重用基類中已經存在的內容,這樣就簡化了代碼的編寫工作。繼承中有三種繼承方式即:public protected private,這三種方式規定了不同的訪問權限,這些權限的檢查由編譯器在語法檢查階段進行,不參與生成最終的機器碼,所以在這裡不對這三中權限進行討論,一下的內容都是采用的共有繼承。
首先看下面的代碼:
class CParent
{
public:
CParent(){
printf("CParent()\n");
}
~CParent(){
printf("~CParent()\n");
}
void setNumber(int n){
m_nParent = n;
}
int getNumber(){
return m_nParent;
}
protected:
int m_nParent;
};
class CChild : public CParent
{
public:
void ShowNumber(int n){
setNumber(n);
m_nChild = 2 *m_nParent;
printf("child:%d\n", m_nChild);
printf("parent:%d\n", m_nParent);
}
protected:
int m_nChild;
};
int main()
{
CChild cc;
cc.ShowNumber(2);
return 0;
}
上面的代碼中定義了一個基類,以及一個對應的派生類,在派生類的函數中,調用和成員m_nParent,我們沒有在派生類中定義這個變量,很明顯這個變量來自於基類,子類會繼承基類中的函數成員和數據成員,下面的匯編代碼展示了它是如何存儲以及如何調用函數的:
41: CChild cc;
004012AD lea ecx,[ebp-14h];將類對象的首地址this放入ecx中
004012B0 call @ILT+5(CChild::CChild) (0040100a);調用構造函數
004012B5 mov dword ptr [ebp-4],0
42: cc.ShowNumber(2);
004012BC push 2
004012BE lea ecx,[ebp-14h]
004012C1 call @ILT+10(CChild::ShowNumber) (0040100f);調用自身的函數
43: return 0;
004012C6 mov dword ptr [ebp-18h],0
004012CD mov dword ptr [ebp-4],0FFFFFFFFh
004012D4 lea ecx,[ebp-14h]
004012D7 call @ILT+25(CChild::~CChild) (0040101e);調用析構函數
004012DC mov eax,dword ptr [ebp-18h]
;構造函數
0040140A mov dword ptr [ebp-4],ecx
0040140D mov ecx,dword ptr [ebp-4];到此ecx和ebp - 4位置的值都是對象的首地址
00401410 call @ILT+35(CParent::CParent) (00401028);調用父類的構造
00401415 mov eax,dword ptr [ebp-4]
;ShowNumber函數
00401339 pop ecx;還原this指針
0040133A mov dword ptr [ebp-4],ecx;ebp - 4存儲的是this指針
31: setNumber(n);
0040133D mov eax,dword ptr [ebp+8];ebp + 8是showNumber參數
00401340 push eax
00401341 mov ecx,dword ptr [ebp-4]
00401344 call @ILT+0(CParent::setNumber) (00401005)
32: m_nChild = 2 *m_nParent;
00401349 mov ecx,dword ptr [ebp-4]
0040134C mov edx,dword ptr [ecx];取this對象的頭4個字節的值到edx中
0040134E shl edx,1;edx左移1位,相當於edx = edx * 2
00401350 mov eax,dword ptr [ebp-4]
00401353 mov dword ptr [eax+4],edx ;將edx的值放入到對象的第4個字節處
33: printf("child:%d\n", m_nChild);
00401356 mov ecx,dword ptr [ebp-4]
00401359 mov edx,dword ptr [ecx+4]
0040135C push edx
0040135D push offset string "child:%d\n" (0042f02c)
00401362 call printf (00401c70)
00401367 add esp,8
34: printf("parent:%d\n", m_nParent);
0040136A mov eax,dword ptr [ebp-4]
0040136D mov ecx,dword ptr [eax]
0040136F push ecx
00401370 push offset string "parent:%d\n" (0042f01c)
00401375 call printf (00401c70)
0040137A add esp,8
;setNumber函數
16: m_nParent = n;
004013CD mov eax,dword ptr [ebp-4]
004013D0 mov ecx,dword ptr [ebp+8]
004013D3 mov dword ptr [eax],ecx;給對象的頭四個字節賦值
從上面的匯編代碼可以看到大致的執行流程,首先調用編譯器提供的默認構造函數,在這個構造函數中調用父類的構造函數,然後在showNumber中調用setNumber為父類的m_nParent賦值,然後為m_nChild賦值,最後執行輸出語句。
上面的匯編代碼在執行為m_nParent賦值時操作的內存地址是this,而為m_nChild賦值時操作的是this + 4通過這一點可以看出,類CChild在內存中的分布,首先在低地址位分步的是基類的成員,高地址為分步的是派生類的成員,我們隨著代碼的執行,查看寄存器和內存的值也發現,m_nParent在低地址位m_nChild在高地址位:
當父類中含有構造函數,而子類中沒有時,編譯器會提供默認構造函數,這個構造只調用父類的構造,而不做其他多余的操作,但是如果子類中構造,而父類中沒有構造,則不會為父類提供默認構造。但是當父類中有虛函數時又例外,這個時候會為父類提供默認構造,以便初始化虛函數表指針。
在析構時,為了可以析構父類會首先調用子類的析構,當析構到父類的部分時,調用父類的構造,也就是說析構的調用順序與構造正好相反。
子類在內存中的排列順序為先依次擺放父類的成員,後安排子類的成員。
C++中的函數符號名稱與C中的有很大的不同,編譯器根據這個符號名稱可以知道這個函數的形參列表,和作用范圍,所以在繼承的情況下,父類的成員函數的作用范圍在父類中,而派生類則包含了父類的成員,所以自然包含了父類的作用范圍,在進行函數調用時,會首先在其自身的范圍中查找,然後再在其父類中查找,因此子類可以調用父類的函數。在子類中將父類的成員放到內存的前段是為了方便子類調用父類中的成員。但是當子類中有對應的函數,這個時候會直接調用子類中的函數,這個時候發生了覆蓋。
當類中定義了其他類成員,並定義了初始化列表時,構造的順序又是怎樣的呢?
class CParent
{
public:
CParent(){
m_nParent = 0;
}
protected:
int m_nParent;
};
class CInit
{
public:
CInit(){
m_nNumber = 0;
}
protected:
int m_nNumber;
};
class CChild : public CParent
{
public:
CChild(): m_nChild(1){}
protected:
CInit m_Init;
int m_nChild;
};
34: CChild cc;
00401288 lea ecx,[ebp-0Ch]
0040128B call @ILT+5(CChild::CChild) (0040100a)
;構造函數
004012C9 pop ecx
004012CA mov dword ptr [ebp-4],ecx
004012CD mov ecx,dword ptr [ebp-4]
004012D0 call @ILT+25(CParent::CParent) (0040101e);先調用父類的構造
004012D5 mov ecx,dword ptr [ebp-4]
004012D8 add ecx,4
004012DB call @ILT+0(CInit::CInit) (00401005);然後調用類成員的構造
004012E0 mov eax,dword ptr [ebp-4]
004012E3 mov dword ptr [eax+8],1;最後調用初始化列表中的操作
004012EA mov eax,dword ptr [ebp-4]
綜上分析,編譯器在對對象進行初始化時是根據各個部分在內存中的排放順序來進行初始化的,就上面的例子來說,最上面的是基類的所以它首先調用的是基類的構造,然後是類的成員,所以接著調用成員對象的構造函數,最後是自身定義的變量,所以最後初始化自身的變量,但是初始化列表中的操作是先於類自身構造函數中的代碼的。
由於父類的成員在內存中的分步是先於派生類自身的成員,所以通過派生類的指針可以很容易尋址到父類的成員,而且可以將派生類的指針轉化為父類進行操作,並且不會出錯,但是反過來將父類的指針轉化為派生類來使用則會造成越界訪問。
下面我們來看一下對於虛表指針的初始化問題,如果在基類中存在虛函數,而且在派生類中重寫這個虛函數的話,編譯器會如何初始化虛表指針。
class CParent
{
public:
virtual void print(){
printf("CParent()\n");
}
};
class CChild : public CParent
{
public:
virtual void print(){
printf("CChild()\n");
}
};
int main()
{
CChild cc;
return 0;
}
;函數地址
@ILT+0(?print@CParent@@UAEXXZ):
00401005 jmp CParent::print
@ILT+10(?print@CChild@@UAEXXZ):
0040100F jmp CChild::print
;派生類構造函數
004012C9 pop ecx
004012CA mov dword ptr [ebp-4],ecx
004012CD mov ecx,dword ptr [ebp-4]
004012D0 call @ILT+30(CParent::CParent) (00401023)
004012D5 mov eax,dword ptr [ebp-4]
004012D8 mov dword ptr [eax],offset CChild::`vftable' (0042f01c)
004012DE mov eax,dword ptr [ebp-4]
;基類構造函數
00401379 pop ecx
0040137A mov dword ptr [ebp-4],ecx
0040137D mov eax,dword ptr [ebp-4]
00401380 mov dword ptr [eax],offset CParent::`vftable' (0042f02c)
上述代碼的基本流程是首先執行基類的構造函數,在基類中首先初始化虛函數指針,從上面的匯編代碼中可以看到,這個虛函數指針的值為0x0042f02c查看這塊內存可以看到,它保存的值為0x00401005上面我們列出的虛函數地址可以看到,這個值正是基類中虛函數的地址。當基類的構造函數調用完成後,接著執行派生類的虛表指針的初始化,將它自身虛函數的地址存入到虛表中。
通過上面的分析可以知道,在派生類中如果重寫了基類中的虛函數,那麼在創建新的類對象時會有兩次虛表指針的初始化操作,第一次是將基類的虛表指針賦值給對象,然後再將自身的虛表指針賦值給對象,將前一次的覆蓋,如果是在基類的構造中調用虛函數,這個時候由於還沒有生成派生類,所以會直接尋址,找到基類中的虛函數,這個時候不會構成多態,但是如果在派生類的構造函數中調用,這個時候已經初始化了虛表指針,會進行虛表的間接尋址調用派生類的虛函數構成多態。
析構函數與構造函數相反,在執行析構時,會首先將虛表指針賦值為當前類的虛表地址,調用當前類的虛函數,然後再將虛表指針賦值為其基類的虛表地址,執行基類的虛函數。
多重繼承的情況與單繼承的情況類似,只是其父類變為多個,首先來分析多重繼承的內存分布情況
class CParent1
{
public:
virtual void fnc1(){
printf("CParent1 fnc1\n");
}
protected:
int m_n1;
};
class CParent2
{
public:
virtual void fnc2(){
printf("CParent2 fnc2\n");
}
protected:
int m_n2;
};
class CChild : public CParent1, public CParent2
{
public:
virtual void fnc1(){
printf("CChild fnc1()\n");
}
virtual void fnc2(){
printf("CChild fnc2()\n");
}
protected:
int m_n3;
};
int main()
{
CChild cc;
CParent1 *p = &cc;
p->fnc1();
CParent2 *p1 = &cc;
p1->fnc2();
p = NULL;
p1 = NULL;
return 0;
}
上述代碼中,CChild類有兩個基類,CParent1 CParent2 ,並且重寫了這兩個類中的函數:fnc1 fnc2,然後在主函數中分別將cc對象轉化為它的兩個基類的指針,通過指針調用虛函數,實現多態。下面是它的反匯編代碼
43: CChild cc;
004012A8 lea ecx,[ebp-14h];對象的this指針
004012AB call @ILT+15(CChild::CChild) (00401014)
44: CParent1 *p = &cc;
004012B0 lea eax,[ebp-14h]
004012B3 mov dword ptr [ebp-18h],eax;[ebp - 18h]是p的值
45: p->fnc1();
004012B6 mov ecx,dword ptr [ebp-18h]
004012B9 mov edx,dword ptr [ecx];對象的頭四個字節是虛函數表指針
004012BB mov esi,esp
004012BD mov ecx,dword ptr [ebp-18h]
004012C0 call dword ptr [edx];通過虛函數地址調用虛函數
;部分代碼略
46: CParent2 *p1 = &cc;
004012C9 lea eax,[ebp-14h];eax = this
004012CC test eax,eax
004012CE je main+48h (004012d8);校驗this是否為空
004012D0 lea ecx,[ebp-0Ch];this指針向下偏移8個字節
004012D3 mov dword ptr [ebp-20h],ecx
004012D6 jmp main+4Fh (004012df)
004012D8 mov dword ptr [ebp-20h],0;如果this為null會將edx賦值為0
004012DF mov edx,dword ptr [ebp-20h];edx = this + 8
004012E2 mov dword ptr [ebp-1Ch],edx;[ebp - 1CH]是p1的值
47: p1->fnc2();
004012E5 mov eax,dword ptr [ebp-1Ch]
004012E8 mov edx,dword ptr [eax]
004012EA mov esi,esp
004012EC mov ecx,dword ptr [ebp-1Ch]
004012EF call dword ptr [edx]
004012F1 cmp esi,esp
004012F3 call __chkesp (00401680)
;CChild構造函數
0040135A mov dword ptr [ebp-4],ecx
0040135D mov ecx,dword ptr [ebp-4]
00401360 call @ILT+40(CParent1::CParent1) (0040102d)
00401365 mov ecx,dword ptr [ebp-4]
00401368 add ecx,8;將指向對象首地址的指針向下偏移了8個字節
0040136B call @ILT+45(CParent2::CParent2) (00401032)
00401370 mov eax,dword ptr [ebp-4]
00401373 mov dword ptr [eax],offset CChild::`vftable' (0042f020)
00401379 mov ecx,dword ptr [ebp-4]
0040137C mov dword ptr [ecx+8],offset CChild::`vftable' (0042f01c)
00401383 mov eax,dword ptr [ebp-4]
;CParent1構造函數
00401469 pop ecx
0040146A mov dword ptr [ebp-4],ecx
0040146D mov eax,dword ptr [ebp-4]
00401470 mov dword ptr [eax],offset CParent1::`vftable' (0042f04c);初始化虛表指針
00401476 mov eax,dword ptr [ebp-4]
;CParent2構造函數
004014F9 pop ecx
004014FA mov dword ptr [ebp-4],ecx
004014FD mov eax,dword ptr [ebp-4]
00401500 mov dword ptr [eax],offset CParent2::`vftable' (0042f064);初始化虛表指針
00401506 mov eax,dword ptr [ebp-4]
;虛函數地址
@ILT+0(?fnc2@CChild@@UAEXXZ):
00401005 jmp CChild::fnc2 (00401400)
@ILT+5(?fnc1@CParent1@@UAEXXZ):
0040100A jmp CParent1::fnc1 (00401490)
@ILT+10(?fnc1@CChild@@UAEXXZ):
0040100F jmp CChild::fnc1 (004013b0)
@ILT+20(?fnc2@CParent2@@UAEXXZ):
00401019 jmp CParent2::fnc2 (00401520)
從上面的匯編代碼中可以看到,在為該類對象分配內存時,會根據繼承的順序,依次調用基類的構造函數,在構造函數中,與單繼承類似,在各個基類的構造中,先將虛表指針初始化為各個基類的虛表地址,然後在調用完各個基類的構造函數後將虛表指針覆蓋為對象自身的虛表地址,唯一不同的是,派生類有多個虛表指針,有幾個派生類就有幾個虛表指針。另外派生類的內存分布與單繼承的分布情況相似,根據繼承順序從低地址到高地址依次擺放,最後是派生類自己定義的部分,每個基類都會在其自身所在位置的首地址處構建一個虛表。
在調用各自基類的構造函數時,並不是籠統的將對象的首地址傳遞給基類的構造函數,而是經過相應的地址偏移之後,將偏移後的地址傳遞給對應的構造。在轉化為父類的指針時也是經過了相應的地址偏移。
在析構時首先析構自身,然後按照與構造相反的順序調用基類的析構函數。
抽象類是不能實例化的類,只要有純虛函數就是一個抽象類。純虛函數是只有定義而沒有實現的函數,由於虛函數的地址需要填入虛表,所以必須提供虛函數的定義,以便編譯器能夠將虛函數的地址放入虛表,所以虛函數必須定義,但是純虛函數不一樣,它不能定義。
class CParent
{
public:
virtual show() = 0;
};
class CChild : public CParent
{
public:
virtual show(){
printf("CChild()\n");
}
};
int main()
{
CChild cc;
CParent *p = &cc;
p->show();
return 0;
}
上面的代碼定義了一個抽象類CParent,而CChild繼承這個抽象類並實現了其中的純虛函數,在主函數中通過基類的指針掉用虛函數,形成多態。
22: CChild cc;
00401288 lea ecx,[ebp-4]
0040128B call @ILT+0(CChild::CChild) (00401005)
23: CParent *p = &cc;
00401290 lea eax,[ebp-4]
00401293 mov dword ptr [ebp-8],eax
24: p->show();
00401296 mov ecx,dword ptr [ebp-8]
00401299 mov edx,dword ptr [ecx]
0040129B mov esi,esp
0040129D mov ecx,dword ptr [ebp-8]
004012A0 call dword ptr [edx]
;CChild構造函數
004012F0 call @ILT+25(CParent::CParent) (0040101e)
004012F5 mov eax,dword ptr [ebp-4]
004012F8 mov dword ptr [eax],offset CChild::`vftable' (0042f01c)
CParent構造函數
00401399 pop ecx
0040139A mov dword ptr [ebp-4],ecx
0040139D mov eax,dword ptr [ebp-4]
004013A0 mov dword ptr [eax],offset CParent::`vftable' (0042f02c)
004013A6 mov eax,dword ptr [ebp-4]
構造函數中仍然是調用了基類的構造函數,並在基類的構造中對虛表指針進行了賦值,但是基類中並沒有定義show函數,而是將它作為純虛函數,那麼虛表中存儲的的是什麼東西呢,這個位置存儲的是一個_purecall函數,主要是為了防止誤調純虛函數。
菱形繼承是最為復雜的一種繼承方式,它結合了單繼承和多繼承
class CGrand
{
public:
virtual void func1(){
printf("CGrand func1()\n");
}
protected:
int m_nNum1;
};
class CParent1 : public CGrand
{
public:
virtual void func2(){
printf("CParent1 func2()\n");
}
virtual void func3(){
printf("CParent1 func3()\n");
}
protected:
int m_nNum2;
};
class CParent2 : public CGrand
{
public:
virtual void func4(){
printf("CParent2 func4()\n");
}
virtual void func5(){
printf("CParent2 func5()\n");
}
protected:
int m_nNum3;
};
class CChild : public CParent1, public CParent2
{
public:
virtual void func2(){
printf("CChild func2()\n");
}
virtual void func4(){
printf("CChild func4()\n");
}
protected:
int m_nNum4;
};
int main()
{
CChild cc;
CParent1 *p1 = &cc;
CParent2 *p2 = &cc;
return 0;
}
上面的代碼中有4個類,其中CGrand類為祖父類,而CParent1 CParent2為父類,他們都派生自組父類,而子類繼承與CParent1 CParent2,根據前面的經驗可以知道sizeof(CGrand) = 4(vt) + 4(int) = 8,而sizeof(CParent1) = sizeof(CParent2) = sizeof(CGrand) + 4(int) = 12, sizeof(CChild) = sizeof(CParent1) + sizeof(CParent2) + 4(int) = 28;
大致可以知道CChild對象的內存分布是CParent1 CParent2 int這種情況,通過反匯編的方式我們可以看出對象的內存分布如下:
內存的分步來看,CParent1 CParent2都繼承自CGrand類,所以他們都有CGrand類的成員,而CChild類繼承自兩個類,所以CGrand類的成員在CChild類中有兩份,所以在調用m_nNum1成員時會產生二義性,編譯器不知道你准備調用那個m_nNum1成員,所以一般這個時候需要指定調用的是哪個部分的m_nNum1成員。同時在轉化為祖父類的時候也會產生二義性。而虛繼承可以有效的解決這個問題。一般來說虛繼承可以有效的避免二義性是因為重復的內容在對象中只有一份。下面對上述例子進行相應的修改,為每個類添加構造函數構造初始化這些成員變量:m_nNum1 = 1;m_nNum2 = 2; m_nNum3 = 3; m_nNum4 = 4;
另外再為CParent1 CParent2類添加虛繼承。這個時候我們運行程序輸入類CChild的大小:sizeof(CChild) = 36;按照之前所說的同樣的內容只保留的一份,那內存大小應該會減少才對,為何突然增大了8個字節呢,下面來看看對象在內存中的分步:
0012FF5C 30 F0 42 00
0012FF60 48 F0 42 00
0012FF64 02 00 00 00
0012FF68 24 F0 42 00
0012FF6C 3C F0 42 00
0012FF70 03 00 00 00
0012FF74 04 00 00 00
0012FF78 20 F0 42 00
0012FF7C 01 00 00 00
上述內存的分步與我們之前想象的有很大的不同,所有變量的確只有一份,但是總內存大小還是變大了,同時它的存儲順序也不是按照我們之前所說的父類的排在子類的前面,而且還多了一些我們並不了解的數據
下面通過反匯編代碼來說明這些數值的作用:
;主函數部分
73: CChild cc;
004012D8 push 1;是否構造祖父類1表示構造,0表示不構造
004012DA lea ecx,[ebp-24h]
004012DD call @ILT+5(CChild::CChild) (0040100a)
74: printf("%d\n", sizeof(cc));
004012E2 push 24h
004012E4 push offset string "%d\n" (0042f01c)
004012E9 call printf (00401a70)
004012EE add esp,8
75: CParent1 *p1 = &cc;
004012F1 lea eax,[ebp-24h]
004012F4 mov dword ptr [ebp-28h],eax
76: CParent2 *p2 = &cc;
004012F7 lea ecx,[ebp-24h]
004012FA test ecx,ecx
004012FC je main+46h (00401306)
004012FE lea edx,[ebp-18h]
00401301 mov dword ptr [ebp-34h],edx
00401304 jmp main+4Dh (0040130d)
00401306 mov dword ptr [ebp-34h],0
0040130D mov eax,dword ptr [ebp-34h]
00401310 mov dword ptr [ebp-2Ch],eax
77: CGrand *p3 = &cc;
00401313 lea ecx,[ebp-24h]
00401316 test ecx,ecx;this指針不為空
00401318 jne main+63h (00401323);不為空則跳轉
0040131A mov dword ptr [ebp-38h],0
00401321 jmp main+70h (00401330)
00401323 mov edx,dword ptr [ebp-20h];edx = 0x0040f048
00401326 mov eax,dword ptr [edx+4];eax = 0x18這個可以通過查看內存獲得
00401329 lea ecx,[ebp+eax-20h]; ebp + eax - 20h = 0x0012ff78, ecx = 0x0012FF78
0040132D mov dword ptr [ebp-38h],ecx;經過偏移後獲得這個地址
00401330 mov edx,dword ptr [ebp-38h]
00401333 mov dword ptr [ebp-30h],edx
;CChild構造
0040135A mov dword ptr [ebp-4],ecx
0040135D cmp dword ptr [ebp+8],0
00401361 je CChild::CChild+42h (00401382)
00401363 mov eax,dword ptr [ebp-4]
00401366 mov dword ptr [eax+4],offset CChild::`vbtable' (0042f048)
0040136D mov ecx,dword ptr [ebp-4]
00401370 mov dword ptr [ecx+10h],offset CChild::`vbtable' (0042f03c)
00401377 mov ecx,dword ptr [ebp-4]
0040137A add ecx,1Ch
0040137D call @ILT+0(CGrand::CGrand) (00401005);this 指針向下偏移1ch,開始構造父類
00401382 push 0;保證父類只構造一次
00401384 mov ecx,dword ptr [ebp-4]
00401387 call @ILT+60(CParent1::CParent1) (00401041)
0040138C push 0
0040138E mov ecx,dword ptr [ebp-4]
00401391 add ecx,0Ch
00401394 call @ILT+65(CParent2::CParent2) (00401046)
00401399 mov edx,dword ptr [ebp-4]
0040139C mov dword ptr [edx],offset CChild::`vftable' (0042f030)
004013A2 mov eax,dword ptr [ebp-4]
004013A5 mov dword ptr [eax+0Ch],offset CChild::`vftable' (0042f024)
004013AC mov ecx,dword ptr [ebp-4]
004013AF mov edx,dword ptr [ecx+4]
004013B2 mov eax,dword ptr [edx+4]
004013B5 mov ecx,dword ptr [ebp-4]
004013B8 mov dword ptr [ecx+eax+4],offset CChild::`vftable' (0042f020)
57: m_nNum4 = 4;
004013C0 mov edx,dword ptr [ebp-4]
004013C3 mov dword ptr [edx+18h],4
58: }
;CGrand構造
00401429 pop ecx
0040142A mov dword ptr [ebp-4],ecx
0040142D mov eax,dword ptr [ebp-4]
00401430 mov dword ptr [eax],offset CGrand::`vftable' (0042f054);虛表指針後期會被替代
10: m_nNum1 = 1;
00401436 mov ecx,dword ptr [ebp-4]
00401439 mov dword ptr [ecx+4],1
11: }
;CParent1構造
04014C9 pop ecx
004014CA mov dword ptr [ebp-4],ecx
004014CD cmp dword ptr [ebp+8],0;不再調用祖父類構造
004014D1 je CParent1::CParent1+38h (004014e8)
004014D3 mov eax,dword ptr [ebp-4]
004014D6 mov dword ptr [eax+4],offset CParent1::`vbtable' (0042f07c)
004014DD mov ecx,dword ptr [ebp-4]
004014E0 add ecx,0Ch
004014E3 call @ILT+0(CGrand::CGrand) (00401005);這個時候會跳過這個構造函數的調用
通過上面的代碼可以看出,為了使得相同的內容只有一份,在程序中額外傳入一個參數作為標記,用於表示是否調用祖父類構造函數,當初始化完祖父類後將此標記置0以後不再初始化,另外程序在每個父類中都多添加了一個四字節的成員用來存儲一個一個偏移地址,以便能正確的將派生類轉化為父類。所以每當多出一個虛繼承就多了一個記錄偏移量的4字節內存,所以這個類總共多出了8個字節。所以這時候的類所占內存大小為28 + 4 * 2 = 36字節。