天天看點

C語言結構體的大小——記憶體對齊和位域的使用

C語言結構體對齊

C語言結構體對齊也是老生常談的話題了。基本上是面試題的必考題。内容雖然很基礎,但一不小心就會弄錯。寫出一個struct,然後sizeof,你會不會經常對結果感到奇怪?sizeof的結果往往都比你聲明的變量總長度要大,這是怎麼回事呢?結合網上的資料和自己的程式設計實踐,總結如下。

首先考慮一個問題,為什麼要設計記憶體對齊的處理方式呢?如果體系結構是不對齊的,成員将會一個挨一個存儲,顯然對齊更浪費了空間。那麼為什麼要使用對齊呢?體系結構的對齊和不對齊,是在時間和空間上的一個權衡:對齊節省了時間。假設一個體系結構的字長為w,那麼它同時就假設了在這種體系結構上對寬度為w的資料的處理最頻繁也是最重要的。它的設計也是從優先提高對w位資料操作的效率來考慮的。

既然記憶體對齊是必要的,那麼結構體到底是怎樣對齊的呢?

       (在沒有定義#pragma pack宏的情況下):

       原則1、資料成員對齊規則:結構(struct或聯合union)的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員按其類型大小和預設對齊參數(32位系統通常預設按4位元組對齊)中較小的一個對齊。每個成員的起始位址%每個成員的自身對齊值=0,否則補空直至滿足條件。比如對于char型資料,其自身對齊值為1,對于short型為2,對于int型為4,double型在32位機大小為8位元組、系統預設對齊方式是4位元組,取其較小者,是以其對齊值是4。

       原則2、結構體作為成員:如果一個結構裡有結構體成員,則按照其成員中自身對齊值最大的那個值對齊。(struct a裡存有struct b,b裡有char,int,double等元素,那b應該從4的整數倍開始存儲。)

       原則3、收尾工作:結構體的總大小,也就是sizeof的結果,必須為所用過的最大對齊參數的整數倍,不夠就補空位元組。比如在32位系統中最大對齊參數是4,如果一個結構中有類型為int、long、float、double等長度等于或大于4的成員,則其總大小必須是4的整數倍。

       這三個原則具體怎樣了解呢?我們看下面幾個例子,通過執行個體來加深了解。

       例1:

typedef struct _A {

                     short a1;

                     short a2;

                     short a3;

                    }A;

typedef struct _B{

                   long a1;

                   short a2;

                  }B;

       sizeof(A) = 6; 這個很好了解,三個short對齊方式都是2。

       sizeof(B) = 8; 這個是不是比預想的大2個位元組?long為4,short為2,整個為8,因為原則3。

       例2:

typedef struct _A{

                    int a;

                    char b;

                    short c;

                    }A;

typedef struct _B{

                   char b;

                   int a;

                   short c;

                    }B;

       sizeof(A) = 8; int為4,char為1,short為2,這裡用到了原則1和原則3。

       sizeof(B) = 12; 是否超出預想範圍?char為1,int為4,short為2,怎麼會是12?還是原則1和原則3。

       深究一下,為什麼是這樣,我們可以看看記憶體裡的布局情況。

                         a           b         c

       A的記憶體布局:1111,    1*,      11

                         b           a           c

       B的記憶體布局:1***,     1111,   11**

       其中星号*表示填充的位元組。A中,b後面為何要補充一個位元組?因為c為short類型,其起始位置要為2的倍數,就是原則1。c的後面沒有補充,因為b和c正好占用4個位元組,整個A占用空間為4的倍數,也就是最大成員int類型的倍數,是以不用補充。

       B中,b是char為1,b後面補充了3個位元組,因為a是int為4,根據原則1,起始位置要為4的倍數,是以b後面要補充3個位元組。c後面補充兩個位元組,根據原則3,整個B占用空間要為4的倍數。如果c後面不補充,整個B的空間為10,不符,是以要補充2個位元組。

       再看一個結構中含有結構成員的例子:

       例3:typedef struct _A{

                     int a;

                     double b;

                     float c;

                    }A;

                typedef struct _B{

                     char e[2];

                     int f;

                     double g; 

                     short h;

                     A i;

                    }B;

       sizeof(A) = 16; 這個比較好了解,int為4,double為8,float為4,double對齊方式取了較小的預設方式,按4位元組對齊。是以整個A的大小為16。

       sizeof(B) = 36; 看看B的記憶體布局。

                        e       f           g          h            i

       B的記憶體布局:11* *,   1111,   11111111, 11 * * ,  1111, 11111111, 1111

       i其實就是A的記憶體布局。i的起始位置要為4的倍數,是以h後面要補齊。把B的記憶體布局弄清楚,有關結構體的對齊方式基本就算掌握了。

       以上講的都是沒有#pragma pack宏的情況,如果有#pragma pack宏,那麼對齊方式就要按照宏的定義來。比如上面的結構體前加#pragma pack(1),記憶體的布局就會完全改變。sizeof(A) = 16; sizeof(B) = 32;

       有了#pragma pack(1),記憶體不會再遵循原則1和原則3了,按1位元組對齊。沒錯,這不正是理想中的沒有記憶體對齊的世界嗎?!

                        a          b            c

       A的記憶體布局:1111,     11111111,   1111

                       e    f         g              h      i

       B的記憶體布局:11, 1111, 1111111, 11 , 1111, 11111111, 1111

       那#pragma pack(2)的結果又是多少呢?#pragma pack(8)呢?留給大家自己思考吧,相信沒有問題。

       還有一種常見的情況,結構體中含位域字段。位域成員不能單獨被取sizeof值。C99規定int、unsigned int和bool可以作為位域類型,但編譯器幾乎都對此作了擴充,允許其它類型類型的存在。

       使用位域的主要目的是壓縮存儲,其大緻規則為:

       1) 如果相鄰位域字段的類型相同,且其位寬之和小于類型的sizeof大小,則後面的字段将緊鄰前一個字段存儲,直到不能容納為止;

       2) 如果相鄰位域字段的類型相同,但其位寬之和大于類型的sizeof大小,則後面的字段将從新的存儲單元開始,其偏移量為其類型大小的整數倍;

       3) 如果相鄰的位域字段的類型不同,則各編譯器的具體實作有差異,gcc通常采用壓縮的方式;

       4) 如果位域字段之間穿插着非位域字段,則不進行壓縮;

       5) 整個結構體的總大小為對齊方式的整數倍。對齊方式去最寬基本類型成員大小和預設對齊方式的較小者。

        還是讓我們來看看例子。

       例4:typedef struct _A{

                     char f1 : 3;

                     char f2 : 4;

                     char f3 : 5;

                     }A;

                     a       b       c

       A的記憶體布局:111,    1111 *,   11111 * * *

       位域類型為char,第1個位元組僅能容納下f1和f2,是以f2被壓縮到第1個位元組中,而f3隻能從下一個位元組開始。是以sizeof(A)的結果為2。

       例5:typedef struct _B{

                    char f1 : 3;

                    short f2 : 4;

                    char f3 : 5;

                    }B;

       由于相鄰位域類型不同,在VC6中其sizeof為6,在gcc中為2。

如果是

typedef struct _B{

                    char f1 : 3;

                    int f2 : 4;

                    char f3 : 5;

                    }B;

在gcc中大小則是4,它們仍然被壓縮在了一起,但是長度必須是基本類型int型大小的整數倍;

如果是

typedef struct _B{

                    char f1 : 3;

                    long long  f2 : 4;

                    char f3 : 5;

                    }B;

在32位系統,gcc編譯後大小仍然是4,它們仍然被壓縮在了一起。雖然long long大小是8,但預設對齊參數是4;

       例6:struct C{

                     char f1 : 3;

                     char f2;

                     char f3 : 5;

                    };

       非位域字段穿插在其中,不會産生壓縮,在VC6和gcc中得到的大小均為3。

       最後順便提一點,在設計結構體的時候,一般會尊照一個習慣,就是把占用空間小的類型排在前面,占用空間大的類型排在後面,這樣可以相對節約一些對齊空間。

繼續閱讀