結(jié)構(gòu)體的內(nèi)存對齊

結(jié)構(gòu)體是一種結(jié)構(gòu)型的數(shù)據(jù),可以把多種不同的基本數(shù)據(jù)類型有結(jié)構(gòu)的組織在一起,定義為一個(gè)新的數(shù)據(jù)類型。那么這種類型的數(shù)據(jù)在內(nèi)存中是如何存放的,有什么規(guī)則要遵循,還是按照數(shù)據(jù)類型所占內(nèi)存大小順序的排列下去就好了,下面就讓我們來一窺究竟。
翠花,上代碼!

struct CHStruct1 {
    double a;   
    char b;     
    int c;      
    short d;    
};
typedef struct CHStruct1 CHStruct1;

可以看到CHStruct1這樣一個(gè)標(biāo)準(zhǔn)的結(jié)構(gòu)體定義,其中包括了一些基本的數(shù)據(jù)類型,那么在內(nèi)存中這個(gè)結(jié)構(gòu)體是如何存放的,占多大的空間,這些問題都需要我們?nèi)パ芯恳幌隆榱擞?jì)算內(nèi)存大小,我們先看下每種數(shù)據(jù)類型在內(nèi)存中所占大小,如下表:


數(shù)據(jù)類型在內(nèi)存所占大小.png

首先我們假設(shè)這個(gè)結(jié)構(gòu)體所占內(nèi)存大小是按照每種數(shù)據(jù)類型的大小加起來的大小,那么我們打印這個(gè)結(jié)構(gòu)體的大小應(yīng)該是8+1+4+2=15,如果是這樣我們就打印下看看:

NSLog(@"%lu",sizeof(struct1));

運(yùn)行之后,實(shí)際打印的結(jié)果是:24。為什么結(jié)果和預(yù)想的不同,下面就要介紹重點(diǎn)了:結(jié)構(gòu)體的內(nèi)存對齊。

  1. 結(jié)構(gòu)struct或聯(lián)合union的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員的對齊按照#pragma pack指定的數(shù)值(或默認(rèn)值)和這個(gè)數(shù)據(jù)成員類型長度中,比較小的那個(gè)進(jìn)行。在上一個(gè)對齊后的地方開始尋找能被當(dāng)前對齊數(shù)值整除的地址.
  2. 結(jié)構(gòu)(或聯(lián)合)的整體對齊規(guī)則:在數(shù)據(jù)成員完成各自對齊之后,結(jié)構(gòu)(或聯(lián)合)本身也要進(jìn)行對齊。主要體現(xiàn)在,最后一個(gè)元素對齊后,后面是否填補(bǔ)空字節(jié),如果填補(bǔ),填補(bǔ)多少?對齊將按照#pragma pack指定的數(shù)值(或默認(rèn)值)和結(jié)構(gòu)(或聯(lián)合)最大數(shù)據(jù)成員類型長度中,比較小的那個(gè)進(jìn)行。
  3. 結(jié)合1、2推斷:當(dāng)#pragma pack(n)n值等于或超過所有數(shù)據(jù)成員類型長度的時(shí)候,這個(gè)n值的大小將不產(chǎn)生任何效果。
  4. 如果嵌套了結(jié)構(gòu)體的情況,嵌套的結(jié)構(gòu)體對齊到自己的最大對齊數(shù)的整數(shù)倍處,結(jié)構(gòu)體的整體大小就是所有最大對齊數(shù)(含嵌套結(jié)構(gòu)體的對齊數(shù))的整數(shù)倍。

看起來是不是很暈,有一種不可描述的感覺,下面就讓我們一步步來拆解,還用上面的結(jié)構(gòu)體來舉例:

//內(nèi)存對齊
//默認(rèn)的對齊數(shù)是8
//linux環(huán)境的默認(rèn)對齊數(shù)4
struct CHStruct1 {
    double a; //double類型占8字節(jié),對齊數(shù)是8,由于是第一個(gè)元素,所以存放的位置是0-7字節(jié)
    char b; //1字節(jié),接著上個(gè)元素位置,這里對齊數(shù)是1,8又是1的整數(shù)倍,所以b成員就存放在a成員的后面
    int c; //4字節(jié),對齊數(shù)是4,上面的元素排到9,4不能被9整除,所以要補(bǔ)齊到12的位置,從下標(biāo)為12的字節(jié)位置開始存放,也就是12,13,14,15這4個(gè)字節(jié)來存放成員c
    short d; //2字節(jié),上面的成員排列到了15,這里要從下標(biāo)16的字節(jié)開始存放,16能夠整除2的對齊數(shù),所以16,17這兩個(gè)字節(jié)存放的是成員d   
};
typedef struct CHStruct1 CHStruct1;

從上面的代碼的注釋中,我們看到這個(gè)結(jié)構(gòu)體在各自成員對齊排列后,最終排列到了下標(biāo)為17的字節(jié)位置,也就是共用了18個(gè)字節(jié)來存放整個(gè)結(jié)構(gòu)體,但剛剛sizeof(struct1)打印的結(jié)果并不是18,這就要引出最后一步整體對齊了,整體對齊意味著整個(gè)結(jié)構(gòu)體占用的內(nèi)存長度是自身成員對齊數(shù)最大的那個(gè)的整數(shù)倍,我們看到所有成員中,最大的對齊數(shù)是成員a,也就是8,那么最終對齊內(nèi)存后,所占用內(nèi)存長度需要是8的整數(shù)倍,剛才計(jì)算所有子成員對齊后使用了18個(gè)字節(jié),要想被8整除,距18最近的數(shù)就是24,那么就要在第18個(gè)字節(jié)后面在補(bǔ)充6個(gè)字節(jié),共24個(gè)字節(jié),這下就該明白了NSLog中打印出來的24是怎么算出來的了。

那么字節(jié)對齊有什么意義呢,為什用更少的內(nèi)存能解決的問題偏偏要浪費(fèi)空間呢?這個(gè)問題就和CPU每次讀取數(shù)據(jù)的方式有關(guān)了:

  1. 平臺原因(移植原因): 不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
  2. 性能原因: 數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對齊。 原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對齊的內(nèi)存訪問僅需要一次訪問。

總的來說,結(jié)構(gòu)體的內(nèi)存對齊是拿空間來換取時(shí)間的做法;

下面讓我們更進(jìn)一步,看看結(jié)構(gòu)體嵌套的情況:

struct CHStruct2 {
    double a;   //8 (0-7)
    int b;      //4 (8 9 10 11)
    char c;     //1 (12)
    short d;    //2 13 (14 15) - 16
};
// 15 -> 16
typedef struct CHStruct2 CHStruct2;
struct CHStruct3 {
    double a;   //8 (0-7)
    int b;      //4 (8 9 10 11)
    char c;     //1 (12)
    short d;    //2 13 (14 15) - 16
    CHStruct2 struct2; //16 (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
};
//32
typedef struct CHStruct3 CHStruct3;
CHStruct2 struct2;
CHStruct3 struct3;
NSLog(@"%lu-%lu",sizeof(struct2),sizeof(struct3)); //打印16-32

打印的結(jié)果不出意料,是:16-32,和注釋中的推算完全符合。struct2占16字節(jié),struct3占32字節(jié),但是這個(gè)計(jì)算結(jié)果真的是注釋中這樣的計(jì)算方式嗎,我們認(rèn)為struct3中的成員struct2是那個(gè)最大對齊數(shù)的元素,最終占用內(nèi)存也是它的整數(shù)倍,這一切看起來似乎很正確,但是我們調(diào)整一下結(jié)構(gòu)再看看:

//減少一些成員,再計(jì)算一次
struct CHStruct4 {
    int b;      //4 (0 1 2 3)
    char c;     //1 (4)
    CHStruct2 struct2; //16 5 (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
};
CHStruct4 struct4;
NSLog(@"%lu",sizeof(struct4));//打印24

如果按照上面代碼注釋中的思路,struct4無疑也應(yīng)該占用32字節(jié),打印結(jié)果卻是24,是不是很費(fèi)解,最大對齊元素應(yīng)該是成員struct2,占用16字節(jié),最終對齊結(jié)果應(yīng)該是16的整數(shù)倍,為什么打印出來是24,我們不妨把結(jié)構(gòu)體CHStruct4中的struct2按照原定義展開來看:

struct CHStruct4 {
    int b;      //4 (0 1 2 3)
    char c;     //1 (4)
//    CHStruct2 struct2; //16 5 (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
    double a;   //8 5 (8 9 10 11 12 13 14 15)
    int f;      //4 (16 17 18 19)
    char g;     //1 (20)
    short d;    //2 21 (22 23) - 24
};
//24
typedef struct CHStruct4 CHStruct4;

這樣看是不是一目了然,同樣的計(jì)算方式,注釋推算的最終結(jié)果變成24,而不是32了,所以我們得出了結(jié)論:在結(jié)構(gòu)體嵌套情況下,計(jì)算結(jié)構(gòu)體最大成員,并不是把嵌套其中的結(jié)構(gòu)體當(dāng)做一個(gè)整體成員去計(jì)算對齊,依然可以把它的成員展開成普通類型,看里面的最大對齊數(shù)成員的對齊數(shù)是多少,并從這個(gè)對齊數(shù)的整數(shù)倍位置開始對齊嵌套的結(jié)構(gòu)體成員,按照對齊數(shù)整數(shù)倍方式逐個(gè)對齊,最終在計(jì)算最大對齊數(shù)(包含嵌套結(jié)構(gòu)總的成員對齊數(shù))的整數(shù)倍。
我們可以修改一下CHStruct4的定義,結(jié)論依然是成立的:

//我們調(diào)換一下CHStruct2中的成員位置
struct LGStruct2 {
    int b;      //4 (0 1 2 3)
    double a;   //8 (8 9 10 11 12 13 14 15)
    char c;     //1 (16)
    short d;    //2 13 (18 19) - 24
};
typedef struct CHStruct2 CHStruct2;

struct CHStruct5 {
    char c;     //1 (0)
    short d;    //2 1 (2 3)
    LGStruct2 struct2;
//    int f;      //4 (8 9 10 11)
//    double a;   //8 12 (16 17 18 19 20 21 22 23)
//    char g;     //1 (24)
//    short h;    //2 25 (26 27) - 32
};
typedef struct LGStruct5 LGStruct5;

struct CHStruct6 {
    char c;     //1 (0)
    LGStruct2 struct2;
//    int f;      //4 (8 9 10 11)
//    double a;   //8 12 (16 17 18 19 20 21 22 23)
//    char g;     //1 (24)
//    short h;    //2 25 (26 27) - 32
};
typedef struct CHStruct6 CHStruct6;
CHStruct5 struct5;
CHStruct6 struct6;
NSLog(@"%lu-%lu",,sizeof(struct5),sizeof(struct6)) //輸出32-32

關(guān)于#pragma pack對齊數(shù)的概念:
1.#pragma pack(n)
每個(gè)特定平臺上的編譯器都有自己的默認(rèn)“對齊系數(shù)”(也叫對齊模數(shù))。程序員可以通過預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”。這里規(guī)定的是上界,只影響對齊單元大于n的成員,對于對齊字節(jié)不大于n的成員沒有影響。

可以認(rèn)為處理器一次性可以從內(nèi)存中讀/寫n個(gè)字節(jié)。對于大小小于n的成員,按照自己的對齊條件對齊,因?yàn)椴徽撛趺捶哦伎梢砸淮涡匀〕觥τ趯R條件大于n個(gè)字節(jié)的成員,成員按照自身的對齊條件對齊和按照n字節(jié)對齊需要相同的讀取次數(shù),但按照n字節(jié)對齊節(jié)省空間。
通過預(yù)編譯命令#pragma pack()取消自定義字節(jié)對齊方式。

也可以寫成:
#pragma pack(push,n)
#pragma pack(pop)

2.__attribute__((aligned (n)))

__attribute__((aligned (n))),讓所作用的結(jié)構(gòu)成員對齊在n字節(jié)自然邊界上。如果結(jié)構(gòu)中有成員的長度大于n,則按照最大成員的長度來對齊。
__attribute__((packed)),取消結(jié)構(gòu)在編譯過程中的優(yōu)化對齊,按照實(shí)際占用字節(jié)數(shù)進(jìn)行對齊。

需要注意的是:內(nèi)存對齊的 對齊數(shù) 取決于 對齊系數(shù) 和 成員的字節(jié)數(shù) 兩者之中的較小值。
舉例說明:

struct test {
    char x1;
    short x2;
    float x3;
    char x4;
}

默認(rèn)情況下,結(jié)構(gòu)的第一個(gè)成員x1,其偏移地址為0,占據(jù)了第1個(gè)字節(jié)。第二個(gè)成員x2為short類型,其起始地址必須2字節(jié)對界,因此,編譯器在x2和x1之間填充了一個(gè)空字節(jié)。結(jié)構(gòu)的第三個(gè)成員x3和第四個(gè)成員x4恰好落在其自然邊界地址上,在它們前面不需要額外的填充字節(jié)。在test結(jié)構(gòu)中,成員x3要求4字節(jié)對齊,是該結(jié)構(gòu)所有成員中要求的最大邊界單元,因而test結(jié)構(gòu)的自然對齊條件為4字節(jié),編譯器在成員x4后面填充了3個(gè)空字節(jié)。整個(gè)結(jié)構(gòu)所占據(jù)空間為12字節(jié)。

當(dāng)使用:#pragma pack(1)//讓編譯器對這個(gè)結(jié)構(gòu)作1字節(jié)對齊

#pragma pack(1) //讓編譯器對這個(gè)結(jié)構(gòu)作1字節(jié)對齊
struct test {
    char x1;
    short x2;
    float x3;
    char x4;
};
#pragma pack() //取消1字節(jié)對齊,恢復(fù)為默認(rèn)4字節(jié)對齊

這時(shí)候sizeof(struct test)的值為8。

同理:使用__attribute__((packed))

#define PACKED __attribute__((packed))
struct PACKED test {
    char x1;
    short x2;
    float x3;
    char x4;
}test;

這時(shí)候sizeof( test)的值仍為8。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容