天天看點

結構體和類

在C++中類與結構體并沒有太大的差別,隻是預設的成員通路權限不同,類預設權限為私有,而結構體為公有,是以在這将它們統一處理,在例子中采用類的方式。

類對象在記憶體中的分布

在類中隻有資料成員占記憶體空間,而類的函數成員主要分布在代碼段中,不占記憶體空間,一般對象所占的記憶體空間大小為sizeof(成員1) + sizeof(成員2) + … + sizeof(成員n)但是有幾種情況不符合這個公式,比如虛函數和繼承,空類,記憶體對齊,靜态資料成員。隻要出現虛函數就會多出4個位元組的空間,作為虛函數表,繼承時需要考慮基類的大小,另外出現靜态成員時靜态成員由于存在于資料段中,并不在類對象的空間中,是以靜态成員不計算在類對象的大小中這些不在此處讨論,主要說明其餘的三種情況:

空類

按照上述公式,空類應該不占記憶體,但是實際情況卻不是這樣,下面來看一個具體的例子:

class Test
{
public:
    int Print(){printf("Hello world!\n");}
};

int main()
{
    Test test;
    printf("%d\n", sizeof(test));
    return 0;
}      

運作程式發現,輸出結果為1,這個結果與我們預想的可能有點不一樣,按理來說,空類中沒有資料成員,應該不占記憶體空間才對,但是我們知道每個類都有一個this指針指向具體的記憶體,以便成員函數的調用,即使定義一個類什麼都不寫,編譯器也會提供預設的構造函數用來初始化類,但是如果類的執行個體不占記憶體空間,那麼該如何初始化?是以編譯器為它配置設定一個1位元組的空間以便初始化this指針。是以空類占一個位元組。

記憶體對齊

下面看這樣一個類

class Test
{
public:
    short s;
    int n;
};      

當在程式中定義這樣一個類,通過sizeof來輸出大小得到的是8,上面的公式又不滿足了,我們知道為了程式的運作效率,編譯器并不會依次申請記憶體用于存儲變量,而會采用記憶體對齊的方式,以犧牲一定記憶體空間的代價來換取程式的效率,這個類的大小為8,也是記憶體對齊的結果,檢視類工各個成員的位址我們發現 n的位址為0x0012ff44,而s的位址為0x0012ff40,s本來是占2個位元組,但是n并沒有出現在其後的42的位置,我所用的VC++6.0預設采用的是8個位元組的對齊方式,假設編譯器采用的是n個位元組的對齊方式,而類中某成員實際所占記憶體空間的大小為m,那麼該成員所在的記憶體位址必須為p的整數倍,而p = min(m, n),是以對于s來說,采用的是2個位元組的對齊方式,配置設定到的首位址為40是2的倍數,而其後的整型成員n占4個位元組,采用上述公式,得到它的記憶體位址應該是4的倍數,是以取其後的44作為它的位址,中間有兩個位元組沒有使用,是以這個類占8個位元組。

下面再來看一個例子:

class Test
{
public:
    short s; //8
    double d; //8
    char c;
};      

通過程式得出目前結果體的大小為24,根據上面的分析,首先在為s配置設定空間的時候采用的是2個位元組的對齊方式,假設配置設定到的位址為0x0012ff40,那麼d采用的是8個位元組的對齊方式,它的位址應該為0x0012ff48,最後為c配置設定記憶體的時候,應該是用1個位元組的對齊方式,總共應該占的空間為8 + 8 + 1 = 17但是結果卻并不是這樣。在記憶體對齊時編譯器實際采用對齊方式是:假設結構體成員的最大成員占n個位元組,編譯器預設采用m個位元組的對齊方式,那麼實際對齊大小應該為min(m, n)的整數倍,是以實際采用的是8個位元組的對齊方式,而結構體的大小應該是實際對齊方式的整數倍,是以占24個位元組。在編寫程式時可以使用#pragma pack(n)的方式來改變編譯器的預設對齊方式。另外對于嵌套定義的結構體,對齊情況也有少許不同。

class One
{
public:
    short s;
    double d;
    char c;
};

class Two
{
    One one;
    int n;
};      

輸出class two的大小為32個位元組,嵌套定義的結構體仍然能夠滿足上述兩個法則,首先其中的成員結構體one大小為24,然後另外一個成員n占4個位元組,得到總共占28個位元組,然後根據第二個對齊的規則在24和8之間取最小值8,可以得到結構體的大小應該為8的整數倍32個位元組。

類的成員函數

類的成員函數在調用時直接利用對象打點調用,在函數中直接使用類中的成員,函數操作的是不同對象的資料成員,能夠達到這個目的實際上類的對象在調用類的成員函數時預設傳入的第一個參數是一個指向這個對象位址的指針叫做this指針,具體this指針的原理看下面一段代碼:

class test
{
private:
    int i;
public:
    test(){i = 0;}
    int GetNum()
    {
        i = 10;
        return i;
    };
};
int main(int argc, char* argv[])
{
    test t;
    t.GetNum();
    return 0;
}      

下面對應的反彙編代碼:

;主函數
24:       test t;
00401278   lea         ecx,[ebp-4]
0040127B   call        @ILT+20(test::test) (00401019)
25:       t.GetNum();
00401280   lea         ecx,[ebp-4]
00401283   call        @ILT+0(test::GetNum) (00401005)
26:       return 0;
00401288   xor         eax,eax

;GetNum()函數
18:           i = 10;
0040130D   mov         eax,dword ptr [ebp-4]
00401310   mov         dword ptr [eax],0Ah
19:           return i;
00401316   mov         ecx,dword ptr [ebp-4]
00401319   mov         eax,dword ptr [ecx]      

在主函數中定義類的對象時首先會調用其構造函數,在調用函數之前首先通過lea指令擷取到對象的首位址并将它儲存到了ecx寄存器中,在函數GetNum中,首先是在函數棧中定義了一個局部變量,将這個局部變量的值指派為10,然後将這個局部變量的值指派到ecx所在位址的記憶體中,最後再将這塊記憶體中的值放到eax中作為參數傳回。通過這部分代碼可以看到,this指針并不是通過參數棧的方式傳遞給成員函數的,而是通過一個寄存器來傳遞,但是成員函數中若有參數,則仍然通過參數棧的方式傳遞參數。通過寄存器傳遞給成員方法作為this指針,然後根據資料成員定義的順序和類型進行指針偏移找到對應的記憶體位址,對其進行操作。

類的靜态成員

靜态資料成員

類的靜态成員與之前所說的函數中的局部靜态變量相似,它們都存儲在資料段中,它們的生命周期與它們所在的位置無關,都是全局的生命周期,它們的可見性被封裝到了它們所在的位置,對于函數中的局部靜态變量來說,隻在函數中可見,對于在檔案中的全局靜态變量來說,它們隻在目前檔案中可見,類中的局部靜态變量可見性隻在類中可見。

類的靜态資料成員的生命周期與類對象的無關,這樣我們可以通過類名::變量名的方式來直接通路這塊記憶體,而不需要通過對象通路,由于靜态資料成員所在的記憶體不在具體的類對象中,是以在C++中所有類的對象中的局部靜态變量都是使用同一塊記憶體區域,随便一個修改了靜态變量的值,其他的對象中,這個靜态變量的值都會發生變化。

靜态函數成員

類中的函數成員也可以是靜态的,下面看一個靜态函數成員的例子。

class test
{
public:
    static void print()
    {
        cout<<"hello world";
    }   
};

int main(int argc, char* argv[])
{
    test t;
    t.print();
    return 0;
}      

下面是對應的彙編代碼:

21:       test t;
22:       t.print();
00401388   call        @ILT+80(test::print) (00401055)      

我們可以看到,在調用類的靜态函數時并沒有取對象的位址到ecx的操作,也就說,靜态成員函數并不會傳遞this指針,由于靜态成員的生命周期與對象無關,可以通過類名直接通路,那麼如果靜态成員函數也需要傳遞this指針的話,那麼對于這種通過類名通路的時候,它要怎麼傳遞this指針呢。

另外由于靜态成員函數不傳遞this指針,這樣會造成另外一個問題,如果需要在這個靜态函數中操作類的資料成員,那麼通過對象調用時,它怎麼能找到這個資料成員所在的位址,另外在還沒有對象,通過類直接調用時,這個資料成員還沒有配置設定記憶體位址,是以說在C++中為了避免這些問題直接規定靜态函數不能調用類的非靜态成員,但是靜态資料成員雖然說由所有類共享,但是能夠找到對應的記憶體位址,是以非靜态成員函數是可以通路靜态資料成員的。

類作為函數參數

前面在寫函數原理的那篇博文時說過結構體是如何參數傳遞的,其實類也是一樣的,當類作為參數時,會調用拷貝構造,拷貝到函數的參數棧中,下面通過一個簡單的例子來說明

class test
{
private:
    char szBuf[255];
public:
    static void print()
    {
        cout<<"hello world";
    }   
};
void printhello(test t)
{
    t.print();
}
int main(int argc, char* argv[])
{
    test t;
    printhello(t);
    return 0;
}      
26:       test t;
27:       printhello(t);
0040141E   sub         esp,100h
00401424   mov         ecx,3Fh
00401429   lea         esi,[ebp-100h]
0040142F   mov         edi,esp
00401431   rep movs    dword ptr [edi],dword ptr [esi]
00401433   movs        word ptr [edi],word ptr [esi]
00401435   movs        byte ptr [edi],byte ptr [esi]
00401436   call        @ILT+130(printhello) (00401087)
0040143B   add         esp,100h      

類作為函數傳回值

繼續閱讀