C語言到C++的過渡

C++是由C演變過來的,兩者必定是有聯繫的。這篇博客主要是過渡C,引出一些C++的基礎概念,下面我會用C來模擬並藉助彙編的方式來幫助大家更好的理解C++。

typedef struct Base
{
    int a;
    int b;
}Base;

int getMax(Base base)
{   
    return base.a >= base.b ? base.a : base.b;
}

int main(int argc, char* argv[])
{
    Base test; //局部變量
    test.a = 1;
    test.b = 3;
    int res = getMax(test);
    printf("%d\r\n",res);
    return 0;
}

首先,對於結構體作爲參數傳遞OK不?運行結果這個可以的,那是這裏是傳遞指針,還是傳遞結構體複製傳遞呢,我們觀察其反彙編

22:       int res = getMax(test);
00401096 8B 45 FC             mov         eax,dword ptr [ebp-4] ;test.a
00401099 50                   push        eax
0040109A 8B 4D F8             mov         ecx,dword ptr [ebp-8] ;test.b
0040109D 51                   push        ecx
0040109E E8 62 FF FF FF       call        @ILT+0(getMax) (00401005)
004010A3 83 C4 08             add         esp,8 ;外平棧
004010A6 89 45 F4             mov         dword ptr [ebp-0Ch],eax

第一發現其使用了堆棧傳參,相當於拷貝了一個副本進去,第二其使用了外平棧 。如果參數過多呢,這裏的話就不貼反彙編了,它會先提升棧頂,棧頂指針當成副本的首地址,對其進行拷貝賦值。

所以在開發過程中,最好不用直接使用結構體直接進行參數的傳遞,而是使用結構體指針,使用修改成如下代碼

int getMax(Base *base)
{   
    return base->a >= base->b ? base->a : base->b;
}

int main(int argc, char* argv[])
{
    Base test;
    test.a = 1;
    test.b = 3;
    int res = getMax(&test);
    printf("%d\r\n",res);
    return 0;
}

 觀察其反彙編,會發現其會壓入一個首地址進去,使用的是外平棧

22:       int res = getMax(&test);
00401096 8D 45 F8             lea         eax,[ebp-8] ;取test的首地址
00401099 50                   push        eax
0040109A E8 70 FF FF FF       call        @ILT+10(getMax) (0040100f)
0040109F 83 C4 04             add         esp,4 ;外平棧
004010A2 89 45 F4             mov         dword ptr [ebp-0Ch],eax

下面再改進代碼,打印下該結構體的大小

結果爲8個字節,沒問題。下面我們升級下,其實對於一個函數來說,放到哪裏都是無所謂的,哪怕是放到結構體裏面

typedef struct Base
{
    int a;
    int b;

    int getMax(Base *base)
    {   
        return base->a >= base->b ? base->a : base->b;
    }

}Base;

可以編譯通過,並且運行結果發現結構體的大小好像並沒有發生變化,還是8個字節,那這個函數放結構體裏面和外面真的一樣麼,下面我們調用下

    //int res = getMax(&test); //error C2065: 'getMax' : undeclared identifier
    int res = test.getMax(&test);

之前的調用不管用了,因爲現在對於編譯器來說,getMax函數是結構體的一部分(成員),但是它並不在這個結構體裏面。

所以我們應該和調用a,b一樣的方式調用它

26:       int res = test.getMax(&test);
00401046 8D 45 F8             lea         eax,[ebp-8] ;test首地址
00401049 50                   push        eax
0040104A 8D 4D F8             lea         ecx,[ebp-8] ;多傳了一個參數
0040104D E8 C2 FF FF FF       call        @ILT+15(Base::getMax) (00401014)
00401052 89 45 F4             mov         dword ptr [ebp-0Ch],eax
; 內平棧

看上面的彙編代碼,和之前的全局函數進行比較,會發現多一個參數放入ecx,這個參數和test的首地址是一樣的,而且是內平棧。這個多傳的參數幹啥用呢,我們需要先修改成如下代碼

typedef struct Base
{
    int a;
    int b;

    int getMax()
    {   
        if(a >= b)
            return a;
        else
            return b;
    }

}Base;



int main(int argc, char* argv[])
{
    Base test;
    test.a = 1;
    test.b = 3;
    int res = test.getMax();
    printf("%d\r\n",sizeof(Base));
    return 0;
}

觀察其反彙編

;-----------------call-----------------------------

25:       int res = test.getMax();
00401046 8D 4D F8             lea         ecx,[ebp-8] ;多傳人的參數
00401049 E8 CB FF FF FF       call        @ILT+20(Base::getMax) (00401019)
0040104E 89 45 F4             mov         dword ptr [ebp-0Ch],eax


;----------------getMax----------------------------

11:       int getMax()
12:       {
00401080 55                   push        ebp
00401081 8B EC                mov         ebp,esp
00401083 83 EC 44             sub         esp,44h
00401086 53                   push        ebx
00401087 56                   push        esi
00401088 57                   push        edi
00401089 51                   push        ecx
0040108A 8D 7D BC             lea         edi,[ebp-44h]
0040108D B9 11 00 00 00       mov         ecx,11h
00401092 B8 CC CC CC CC       mov         eax,0CCCCCCCCh
00401097 F3 AB                rep stos    dword ptr [edi]
00401099 59                   pop         ecx ;多傳入的參數 test首地址
0040109A 89 4D FC             mov         dword ptr [ebp-4],ecx ;放入 ebp-4
13:           if(a >= b)
0040109D 8B 45 FC             mov         eax,dword ptr [ebp-4]
004010A0 8B 4D FC             mov         ecx,dword ptr [ebp-4]
004010A3 8B 10                mov         edx,dword ptr [eax]   ;成員 a
004010A5 3B 51 04             cmp         edx,dword ptr [ecx+4] ;成員 b
004010A8 7C 07                jl          Base::getMax+31h (004010b1) ;比較
14:               return a;
004010AA 8B 45 FC             mov         eax,dword ptr [ebp-4]
004010AD 8B 00                mov         eax,dword ptr [eax]
004010AF EB 06                jmp         Base::getMax+37h (004010b7)
15:           else
16:               return b;
004010B1 8B 4D FC             mov         ecx,dword ptr [ebp-4]
004010B4 8B 41 04             mov         eax,dword ptr [ecx+4]
17:       }
004010B7 5F                   pop         edi
004010B8 5E                   pop         esi
004010B9 5B                   pop         ebx
004010BA 8B E5                mov         esp,ebp
004010BC 5D                   pop         ebp
004010BD C3                   ret

看完其彙編,那麼說明如果我們把函數放到結構體裏面,編譯器會幫我們傳遞一個參數進來,就是當前這個結構體的首地址。在函數內會根據其傳入的首地址找到對應的成員數據再進行操作。

所以,我們爲什麼要把函數放入結構體裏面呢?

對於整個結構體的大小來說沒有變化,放裏面的話,當前的編譯器會替我們傳遞一個值,什麼值呢,就是當前結構體的首地址傳入到函數。

爲什麼要有C++呢?

C++可以做的任何事C都能做,之所以學習C++,因爲只要你遵守C++的語法,編譯器會幫我們添加很多很多的額外功能,所以學習C++語法是學習相對於C來說編譯器幫我們做了什麼。(當然除此之外還有面向對象的思想)

簡單來說,自己寫的代碼是C,編譯器幫我們加的代碼就是C++

什麼是封裝?

把函數扔到結構體裏面就是封裝,因爲這樣使用結構體變量比較方便

什麼是類?

帶函數的結構體就是類,結構體類型 -> 類

什麼是對象?

通過結構體類型創建的變量

什麼是成員函數?

結構體裏面的函數就是成員函數。在外面寫的函數可以認爲是全局的,成員函數編譯器會記着,這個函數是屬於哪個類,底層和全局函數沒有任何區別,實際上成員函數並不真正的屬於類,只是編譯器認爲屬於類。

什麼是this指針?

this指針就是當前這個結構體對象的地址,編譯器會默認傳進去的,也就是之前編譯器放到ECX裏面的值(參數個數確定時,不定 長參數會最後一個傳參)。這個this指針不管你用不用,編譯器都會幫你傳遞這個值,這樣子才能保證成員函數在邏輯上封裝(編譯器限制),編譯時獨立(底層和普通函數無區別)

下面我們可以來模擬下

int main(int argc, char* argv[])
{
    /*Base test;
    test.a = 1;
    test.b = 3;
    res = test.getMax();*/

    //模擬上面代碼
    char test[8] = {0}; //test
    test[0] = 1; //a
    test[4] = 3; //b

    int res = 0;

    //cannot convert from 'int (__thiscall Base::*)(void)' to 'int (__cdecl *)(void)'
    //說明結構體內的函數傳參數約定爲: __thiscall
    //int (*fun)() =  &Base::getMax;
    int (Base::*fun)() =  &Base::getMax; //ok 獲取成員函數地址
    __asm
    {
        lea ecx,test ;模擬this傳參
        call fun
        mov res,eax
    }

    printf("%d\r\n",res);
	return 0;
}

上面我們使用了test數組模擬了結構體的內存結構,然後取其首地址給ECX,當成this指針傳參,最後運行結果也正確。

下面我們再來探究下this指針:

    int getMax()
    {   
        //cannot convert from 'struct Base *const ' to 'char *'
        //char *temp = this;  error
        if(a >= b)
            return a;
        else
            return b;
    }

將this指針強轉char*編譯報錯,可以根據報錯看出this指針是個 Base *const類型的指針, 這是什麼類型的指針呢,我們可以反着讀

struct Base *const this;
this is a const pointer to Base struct
this 是一個常量指針指向了Base結構體

那麼說明該指針也是不能++和--的,因爲是個常量。而且this指針的作用只是指向該結構體對象的首地址,無其他含義,沒必要做運算。

Over,最後再記錄一個知識點,空結構體佔多少字節?一字節,爲啥,因爲結構體裏面只有成員函數怎麼辦,調用函數得傳對象首地址(this指針),所以一字節相當於佔個位,方便獲取地址。

最後聲明下,該篇只是爲了更好的幫助理解C++,所以把結構體當成類來學習,雖然其本質上是沒啥區別的,主要注意下,純C文件是不支持這些語法的哈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章