程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++程序開發之繼承分析

C++程序開發之繼承分析

編輯:關於C++

面向對象的三大特性之一就是繼承,繼承運行我麼重用基類中已經存在的內容,這樣就簡化了代碼的編寫工作。繼承中有三種繼承方式即: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)
內存地址 存儲的值 0012FF6C 20 F0 42 00 0012FF70 CC CC CC CC 0012FF74 1C F0 42 00 0012FF78 CC CC CC CC 0012FF7C CC CC CC CC

從上面的匯編代碼中可以看到,在為該類對象分配內存時,會根據繼承的順序,依次調用基類的構造函數,在構造函數中,與單繼承類似,在各個基類的構造中,先將虛表指針初始化為各個基類的虛表地址,然後在調用完各個基類的構造函數後將虛表指針覆蓋為對象自身的虛表地址,唯一不同的是,派生類有多個虛表指針,有幾個派生類就有幾個虛表指針。另外派生類的內存分布與單繼承的分布情況相似,根據繼承順序從低地址到高地址依次擺放,最後是派生類自己定義的部分,每個基類都會在其自身所在位置的首地址處構建一個虛表。
在調用各自基類的構造函數時,並不是籠統的將對象的首地址傳遞給基類的構造函數,而是經過相應的地址偏移之後,將偏移後的地址傳遞給對應的構造。在轉化為父類的指針時也是經過了相應的地址偏移。
在析構時首先析構自身,然後按照與構造相反的順序調用基類的析構函數。

抽象類

抽象類是不能實例化的類,只要有純虛函數就是一個抽象類。純虛函數是只有定義而沒有實現的函數,由於虛函數的地址需要填入虛表,所以必須提供虛函數的定義,以便編譯器能夠將虛函數的地址放入虛表,所以虛函數必須定義,但是純虛函數不一樣,它不能定義。

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字節。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved