c中字節(jié)序問題_第1頁
c中字節(jié)序問題_第2頁
c中字節(jié)序問題_第3頁
c中字節(jié)序問題_第4頁
c中字節(jié)序問題_第5頁
已閱讀5頁,還剩16頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

1、C語言字節(jié)對齊問題詳解時(shí)間:2014-07-19 00:37:40      閱讀:2315      評論:0      收藏:0      點(diǎn)我收藏+標(biāo)簽:算法   class   代碼   style   int   html

2、0;     引言     考慮下面的結(jié)構(gòu)體定義:1 typedef struct2 char c1;3 short s; 4 char c2; 5 int i;6 T_FOO;     假設(shè)這個(gè)結(jié)構(gòu)體的成員在內(nèi)存中是緊湊排列的,且c1的起始地址是0,則s的地址就是1,c2的地址是3,i的地址是4。     現(xiàn)在,我們編寫一個(gè)簡單的程序:1 int main(void) 2 T_FOO a; 3 printf("c1 -> %d, s -> %

3、d, c2 -> %d, i -> %dn", 4 (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,5 (unsigned int)(void*)&a.s - (unsigned int)(void*)&a, 6 (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, 7 (unsigned int)(void*)&a.i - (unsigned int)(void*)&a); 8 return 0;

4、9      運(yùn)行后輸出: 1 c1 -> 0, s -> 2, c2 -> 4, i -> 8     為什么會這樣?這就是字節(jié)對齊導(dǎo)致的問題。     本文在參考諸多資料的基礎(chǔ)上,詳細(xì)介紹常見的字節(jié)對齊問題。因成文較早,資料來源大多已不可考,敬請諒解。  一  什么是字節(jié)對齊     現(xiàn)代計(jì)算機(jī)中,內(nèi)存空間按照字節(jié)劃分,理論上可以從任何起始地址訪問任意類型的變量。但實(shí)際中在訪問特定類型變量時(shí)經(jīng)常在特定的內(nèi)存地

5、址訪問,這就需要各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序一個(gè)接一個(gè)地存放,這就是對齊。 二  對齊的原因和作用     不同硬件平臺對存儲空間的處理上存在很大的不同。某些平臺對特定類型的數(shù)據(jù)只能從特定地址開始存取,而不允許其在內(nèi)存中任意存放。例如Motorola 68000 處理器不允許16位的字存放在奇地址,否則會觸發(fā)異常,因此在這種架構(gòu)下編程必須保證字節(jié)對齊。     但最常見的情況是,如果不按照平臺要求對數(shù)據(jù)存放進(jìn)行對齊,會帶來存取效率上的損失。比如32位的Intel處理器通過總線訪問(包括讀和寫)

6、內(nèi)存數(shù)據(jù)。每個(gè)總線周期從偶地址開始訪問32位內(nèi)存數(shù)據(jù),內(nèi)存數(shù)據(jù)以字節(jié)為單位存放。如果一個(gè)32位的數(shù)據(jù)沒有存放在4字節(jié)整除的內(nèi)存地址處,那么處理器就需要2個(gè)總線周期對其進(jìn)行訪問,顯然訪問效率下降很多。     因此,通過合理的內(nèi)存對齊可以提高訪問效率。為使CPU能夠?qū)?shù)據(jù)進(jìn)行快速訪問,數(shù)據(jù)的起始地址應(yīng)具有“對齊”特性。比如4字節(jié)數(shù)據(jù)的起始地址應(yīng)位于4字節(jié)邊界上,即起始地址能夠被4整除。     此外,合理利用字節(jié)對齊還可以有效地節(jié)省存儲空間。但要注意,在32位機(jī)中使用1字節(jié)或2字節(jié)對齊,反而會降低變量訪問速度。因此需要考慮處理器類型。還

7、應(yīng)考慮編譯器的類型。在VC/C+和GNU GCC中都是默認(rèn)是4字節(jié)對齊。   三  對齊的分類和準(zhǔn)則     主要基于Intel X86架構(gòu)介紹結(jié)構(gòu)體對齊和棧內(nèi)存對齊,位域本質(zhì)上為結(jié)構(gòu)體類型。     對于Intel X86平臺,每次分配內(nèi)存應(yīng)該是從4的整數(shù)倍地址開始分配,無論是對結(jié)構(gòu)體變量還是簡單類型的變量。3.1 結(jié)構(gòu)體對齊     在C語言中,結(jié)構(gòu)體是種復(fù)合數(shù)據(jù)類型,其構(gòu)成元素既可以是基本數(shù)據(jù)類型(如int、long、float等)的變量,也可以是一些復(fù)合數(shù)據(jù)

8、類型(如數(shù)組、結(jié)構(gòu)體、聯(lián)合等)的數(shù)據(jù)單元。編譯器為結(jié)構(gòu)體的每個(gè)成員按照其自然邊界(alignment)分配空間。各成員按照它們被聲明的順序在內(nèi)存中順序存儲,第一個(gè)成員的地址和整個(gè)結(jié)構(gòu)的地址相同。     字節(jié)對齊的問題主要就是針對結(jié)構(gòu)體。3.1.1 簡單示例     先看個(gè)簡單的例子(32位,X86處理器,GCC編譯器):    【例1】設(shè)結(jié)構(gòu)體如下定義: 1 struct A 2 int a; 3 char b; 4 short c; 5 ; 6 struct B 7 char b; 8 int a; 9 sho

9、rt c;10 ;     已知32位機(jī)器上各數(shù)據(jù)類型的長度為:char為1字節(jié)、short為2字節(jié)、int為4字節(jié)、long為4字節(jié)、float為4字節(jié)、double為8字節(jié)。那么上面兩個(gè)結(jié)構(gòu)體大小如何呢?     結(jié)果是:sizeof(strcut A)值為8;sizeof(struct B)的值卻是12。      結(jié)構(gòu)體A中包含一個(gè)4字節(jié)的int數(shù)據(jù),一個(gè)1字節(jié)char數(shù)據(jù)和一個(gè)2字節(jié)short數(shù)據(jù);B也一樣。按理說A和B大小應(yīng)該都是7字節(jié)。之所以出現(xiàn)上述結(jié)果,就是因?yàn)榫幾g器要對數(shù)據(jù)成員

10、在空間上進(jìn)行對齊。3.1.2 對齊準(zhǔn)則     先來看四個(gè)重要的基本概念:     1) 數(shù)據(jù)類型自身的對齊值:char型數(shù)據(jù)自身對齊值為1字節(jié),short型數(shù)據(jù)為2字節(jié),int/float型為4字節(jié),double型為8字節(jié)。     2) 結(jié)構(gòu)體或類的自身對齊值:其成員中自身對齊值最大的那個(gè)值。     3) 指定對齊值:#pragma pack (value)時(shí)的指定對齊值value。     4) 數(shù)據(jù)成員、結(jié)構(gòu)體和類的有效對齊值:自身對齊

11、值和指定對齊值中較小者,即有效對齊值=min自身對齊值,當(dāng)前指定的pack值。     基于上面這些值,就可以方便地討論具體數(shù)據(jù)結(jié)構(gòu)的成員和其自身的對齊方式。     其中,有效對齊值N是最終用來決定數(shù)據(jù)存放地址方式的值。有效對齊N表示“對齊在N上”,即該數(shù)據(jù)的“存放起始地址%N=0”。而數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)變量都是按定義的先后順序存放。第一個(gè)數(shù)據(jù)變量的起始地址就是數(shù)據(jù)結(jié)構(gòu)的起始地址。結(jié)構(gòu)體的成員變量要對齊存放,結(jié)構(gòu)體本身也要根據(jù)自身的有效對齊值圓整(即結(jié)構(gòu)體成員變量占用總長度為結(jié)構(gòu)體有效對齊值的整數(shù)倍)。    &#

12、160;以此分析3.1.1節(jié)中的結(jié)構(gòu)體B:     假設(shè)B從地址空間0x0000開始存放,且指定對齊值默認(rèn)為4(4字節(jié)對齊)。成員變量b的自身對齊值是1,比默認(rèn)指定對齊值4小,所以其有效對齊值為1,其存放地址0x0000符合0x0000%1=0。成員變量a自身對齊值為4,所以有效對齊值也為4,只能存放在起始地址為0x00040x0007四個(gè)連續(xù)的字節(jié)空間中,符合0x0004%4=0且緊靠第一個(gè)變量。變量c自身對齊值為 2,所以有效對齊值也是2,可存放在0x00080x0009兩個(gè)字節(jié)空間中,符合0x0008%2=0。所以從0x00000x0009存放的都是B內(nèi)容。

13、     再看數(shù)據(jù)結(jié)構(gòu)B的自身對齊值為其變量中最大對齊值(這里是b)所以就是4,所以結(jié)構(gòu)體的有效對齊值也是4。根據(jù)結(jié)構(gòu)體圓整的要求, 0x00000x0009=10字節(jié),(102)40。所以0x0000A0x000B也為結(jié)構(gòu)體B所占用。故B從0x0000到0x000B 共有12個(gè)字節(jié),sizeof(struct B)=12。     之所以編譯器在后面補(bǔ)充2個(gè)字節(jié),是為了實(shí)現(xiàn)結(jié)構(gòu)數(shù)組的存取效率。試想如果定義一個(gè)結(jié)構(gòu)B的數(shù)組,那么第一個(gè)結(jié)構(gòu)起始地址是0沒有問題,但是第二個(gè)結(jié)構(gòu)呢?按照數(shù)組的定義,數(shù)組中所有元素都緊挨著。如果我們不把結(jié)構(gòu)體

14、大小補(bǔ)充為4的整數(shù)倍,那么下一個(gè)結(jié)構(gòu)的起始地址將是0x0000A,這顯然不能滿足結(jié)構(gòu)的地址對齊。因此要把結(jié)構(gòu)體補(bǔ)充成有效對齊大小的整數(shù)倍。其實(shí)對于char/short/int/float/double等已有類型的自身對齊值也是基于數(shù)組考慮的,只是因?yàn)檫@些類型的長度已知,所以他們的自身對齊值也就已知。      上面的概念非常便于理解,不過個(gè)人還是更喜歡下面的對齊準(zhǔn)則。     結(jié)構(gòu)體字節(jié)對齊的細(xì)節(jié)和具體編譯器實(shí)現(xiàn)相關(guān),但一般而言滿足三個(gè)準(zhǔn)則:     1) 結(jié)構(gòu)體變量的首地址能夠被其最寬基本類型

15、成員的大小所整除;     2) 結(jié)構(gòu)體每個(gè)成員相對結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍,如有需要編譯器會在成員之間加上填充字節(jié)(internal adding);     3) 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,如有需要編譯器會在最末一個(gè)成員之后加上填充字節(jié)trailing padding。     對于以上規(guī)則的說明如下:     第一條:編譯器在給結(jié)構(gòu)體開辟空間時(shí),首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類型,然后尋找內(nèi)存地址能被該基本數(shù)據(jù)類

16、型所整除的位置,作為結(jié)構(gòu)體的首地址。將這個(gè)最寬的基本數(shù)據(jù)類型的大小作為上面介紹的對齊模數(shù)。     第二條:為結(jié)構(gòu)體的一個(gè)成員開辟空間之前,編譯器首先檢查預(yù)開辟空間的首地址相對于結(jié)構(gòu)體首地址的偏移是否是本成員大小的整數(shù)倍,若是,則存放本成員,反之,則在本成員和上一個(gè)成員之間填充一定的字節(jié),以達(dá)到整數(shù)倍的要求,也就是將預(yù)開辟空間的首地址后移幾個(gè)字節(jié)。     第三條:結(jié)構(gòu)體總大小是包括填充字節(jié),最后一個(gè)成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最后填充幾個(gè)字節(jié)以達(dá)到本條要求。    【例2】假設(shè)4字節(jié)對齊,

17、以下程序的輸出結(jié)果是多少? 1 /* OFFSET宏定義可取得指定結(jié)構(gòu)體某成員在結(jié)構(gòu)體內(nèi)部的偏移 */ 2 #define OFFSET(st, field) (size_t)&(st*)0)->field) 3 typedef struct 4 char a; 5 short b; 6 char c; 7 int d; 8 char e3; 9 T_Test;10 11 int main(void) 12 printf("Size = %dn a-%d, b-%d, c-%d, d-%dn e0-%d, e1-%d, e2-%dn",13 sizeof(T_

18、Test), OFFSET(T_Test, a), OFFSET(T_Test, b),14 OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e0),15 OFFSET(T_Test, e1),OFFSET(T_Test, e2);16 return 0;17      執(zhí)行后輸出如下:1 Size = 162 a-0, b-2, c-4, d-83 e0-12, e1-13, e2-14     下面來具體分析:     首先char a占用1個(gè)

19、字節(jié),沒問題。     short b本身占用2個(gè)字節(jié),根據(jù)上面準(zhǔn)則2,需要在b和a之間填充1個(gè)字節(jié)。     char c占用1個(gè)字節(jié),沒問題。     int d本身占用4個(gè)字節(jié),根據(jù)準(zhǔn)則2,需要在d和c之間填充3個(gè)字節(jié)。     char e3;本身占用3個(gè)字節(jié),根據(jù)原則3,需要在其后補(bǔ)充1個(gè)字節(jié)。     因此,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字節(jié)。3.1.3 對齊的隱

20、患3.1.3.1 數(shù)據(jù)類型轉(zhuǎn)換     代碼中關(guān)于對齊的隱患,很多是隱式的。例如,在強(qiáng)制類型轉(zhuǎn)換的時(shí)候: 1 int main(void) 2 unsigned int i = 0x12345678; 3 4 unsigned char *p = (unsigned char *)&i; 5 *p = 0x00; 6 unsigned short *p1 = (unsigned short *)(p+1); 7 *p1 = 0x0000; 8 9 return 0;10      最后兩句代碼,從奇數(shù)邊界去訪問unsigned

21、 short型變量,顯然不符合對齊的規(guī)定。在X86上,類似的操作只會影響效率;但在MIPS或者SPARC上可能導(dǎo)致error,因?yàn)樗鼈円蟊仨氉止?jié)對齊。     又如對于3.1.1節(jié)的結(jié)構(gòu)體struct B,定義如下函數(shù):1 void Func(struct B *p)2 /Code3      在函數(shù)體內(nèi)如果直接訪問p->a,則很可能會異常。因?yàn)镸IPS認(rèn)為a是int,其地址應(yīng)該是4的倍數(shù),但p->a的地址很可能不是4的倍數(shù)。     如果p的地址不在對齊邊界上就可能出問題,比如p來自一個(gè)跨

22、CPU的數(shù)據(jù)包(多種數(shù)據(jù)類型的數(shù)據(jù)被按順序放置在一個(gè)數(shù)據(jù)包中傳輸),或p是經(jīng)過指針移位算出來的。因此要特別注意跨CPU數(shù)據(jù)的接口函數(shù)對接口輸入數(shù)據(jù)的處理,以及指針移位再強(qiáng)制轉(zhuǎn)換為結(jié)構(gòu)指針進(jìn)行訪問時(shí)的安全性。      解決方式如下:     1) 定義一個(gè)此結(jié)構(gòu)的局部變量,用memmove方式將數(shù)據(jù)拷貝進(jìn)來。1 void Func(struct B *p)2 struct B tData;3 memmove(&tData, p, sizeof(struct B);4 /此后可安全訪問tData.a,因?yàn)榫幾g器已將tD

23、ata分配在正確的起始地址上5      注意:如果能確定p的起始地址沒問題,則不需要這么處理;如果不能確定(比如跨CPU輸入數(shù)據(jù)、或指針移位運(yùn)算出來的數(shù)據(jù)要特別小心),則需要這樣處理。     2) 用#pragma pack (1)將STRUCT_T定義為1字節(jié)對齊方式。3.1.3.2 處理器間數(shù)據(jù)通信     處理器間通過消息(對于C/C+而言就是結(jié)構(gòu)體)進(jìn)行通信時(shí),需要注意字節(jié)對齊以及字節(jié)序的問題。     大多數(shù)編譯器提供內(nèi)存對其的選項(xiàng)供用戶使用。這樣用戶可以根據(jù)處

24、理器的情況選擇不同的字節(jié)對齊方式。例如C/C+編譯器提供的#pragma pack(n) n=1,2,4等,讓編譯器在生成目標(biāo)文件時(shí),使內(nèi)存數(shù)據(jù)按照指定的方式排布在1,2,4等字節(jié)整除的內(nèi)存地址處。     然而在不同編譯平臺或處理器上,字節(jié)對齊會造成消息結(jié)構(gòu)長度的變化。編譯器為了使字節(jié)對齊可能會對消息結(jié)構(gòu)體進(jìn)行填充,不同編譯平臺可能填充為不同的形式,大大增加處理器間數(shù)據(jù)通信的風(fēng)險(xiǎn)。      下面以32位處理器為例,提出一種內(nèi)存對齊方法以解決上述問題。     對于本地使用的數(shù)據(jù)結(jié)構(gòu),為提高內(nèi)

25、存訪問效率,采用四字節(jié)對齊方式;同時(shí)為了減少內(nèi)存的開銷,合理安排結(jié)構(gòu)體成員的位置,減少四字節(jié)對齊導(dǎo)致的成員之間的空隙,降低內(nèi)存開銷。     對于處理器之間的數(shù)據(jù)結(jié)構(gòu),需要保證消息長度不會因不同編譯平臺或處理器而導(dǎo)致消息結(jié)構(gòu)體長度發(fā)生變化,使用一字節(jié)對齊方式對消息結(jié)構(gòu)進(jìn)行緊縮;為保證處理器之間的消息數(shù)據(jù)結(jié)構(gòu)的內(nèi)存訪問效率,采用字節(jié)填充的方式自己對消息中成員進(jìn)行四字節(jié)對齊。     數(shù)據(jù)結(jié)構(gòu)的成員位置要兼顧成員之間的關(guān)系、數(shù)據(jù)訪問效率和空間利用率。順序安排原則是:四字節(jié)的放在最前面,兩字節(jié)的緊接最后一個(gè)四字節(jié)成員,一字節(jié)緊接最后一個(gè)兩字

26、節(jié)成員,填充字節(jié)放在最后。     舉例如下:1 typedef struct tag_T_MSG2 long ParaA;3 long ParaB;4 short ParaC;5 char ParaD;6 char Pad; /填充字節(jié)7 T_MSG;3.1.3.3 排查對齊問題     如果出現(xiàn)對齊或者賦值問題可查看:     1) 編譯器的字節(jié)序大小端設(shè)置;     2) 處理器架構(gòu)本身是否支持非對齊訪問;     3) 如果支持看設(shè)置對齊

27、與否,如果沒有則看訪問時(shí)需要加某些特殊的修飾來標(biāo)志其特殊訪問操作。 3.1.4 更改對齊方式     主要是更改C編譯器的缺省字節(jié)對齊方式。        在缺省情況下,C編譯器為每一個(gè)變量或是數(shù)據(jù)單元按其自然對界條件分配空間。一般地,可以通過下面的方法來改變?nèi)笔〉膶鐥l件:· 使用偽指令#pragma pack(n):C編譯器將按照n個(gè)字節(jié)對齊;· 使用偽指令#pragma pack(): 取消自定義字節(jié)對齊方式。     另外,還有如下的一種方式(GC

28、C特有語法):· _attribute(aligned (n): 讓所作用的結(jié)構(gòu)成員對齊在n字節(jié)自然邊界上。如果結(jié)構(gòu)體中有成員的長度大于n,則按照最大成員的長度來對齊。· _attribute_ (packed): 取消結(jié)構(gòu)在編譯過程中的優(yōu)化對齊,按照實(shí)際占用字節(jié)數(shù)進(jìn)行對齊。    【注】_attribute_機(jī)制是GCC的一大特色,可以設(shè)置函數(shù)屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute)。詳細(xì)介紹請參考:      

29、60;   下面具體針對MS VC/C+ 6.0編譯器介紹下如何修改編譯器默認(rèn)對齊值。     1) VC/C+ IDE環(huán)境中,可在Project|Settings,C/C+選項(xiàng)卡Category的Code Generation選項(xiàng)的Struct Member Alignment中修改,默認(rèn)是8字節(jié)。      VC/C+中的編譯選項(xiàng)有/Zp1|2|4|8|16,/Zpn表示以n字節(jié)邊界對齊。n字節(jié)邊界對齊是指一個(gè)成員的地址必須安排在成員的尺寸的整數(shù)倍地址上或者是n的整數(shù)倍地址上,取它們中的最小值。亦即:min(s

30、izeof(member), n)。     實(shí)際上,1字節(jié)邊界對齊也就表示結(jié)構(gòu)成員之間沒有空洞。     /Zpn選項(xiàng)應(yīng)用于整個(gè)工程,影響所有參與編譯的結(jié)構(gòu)體。在Struct member alignment中可選擇不同的對齊值來改變編譯選項(xiàng)。     2) 在編碼時(shí),可用#pragma pack動態(tài)修改對齊值。具體語法說明見附錄5.3節(jié)。     自定義對齊值后要用#pragma pack()來還原,否則會對后面的結(jié)構(gòu)造成影響。     【例

31、3】分析如下結(jié)構(gòu)體C:1 #pragma pack(2) /指定按2字節(jié)對齊2 struct C3 char b;4 int a;5 short c;6 ;7 #pragma pack() /取消指定對齊,恢復(fù)缺省對齊     變量b自身對齊值為1,指定對齊值為2,所以有效對齊值為1,假設(shè)C從0x0000開始,則b存放在0x0000,符合0x0000%1= 0;變量a自身對齊值為4,指定對齊值為2,所以有效對齊值為2,順序存放在0x00020x0005四個(gè)連續(xù)字節(jié)中,符合0x0002%2=0。變量c的自身對齊值為2,所以有效對齊值為2,順序存放在0x00060x0

32、007中,符合 0x0006%2=0。所以從0x0000到0x00007共八字節(jié)存放的是C的變量。C的自身對齊值為4,所以其有效對齊值為2。又8%2=0,C只占用0x00000x0007的八個(gè)字節(jié)。所以sizeof(struct C) = 8。     注意,結(jié)構(gòu)體對齊到的字節(jié)數(shù)并非完全取決于當(dāng)前指定的pack值,如下:1 #pragma pack(8)2 struct D3 char b;4 short a;5 char c;6 ;7 #pragma pack()     雖然#pragma pack(8),但依然按照兩字節(jié)對齊,所

33、以sizeof(struct D)的值為6。因?yàn)椋簩R到的字節(jié)數(shù) = min當(dāng)前指定的pack值,最大成員大小。     另外,GNU GCC編譯器中按1字節(jié)對齊可寫為以下形式:1 #define GNUC_PACKED _attribute_(packed)2 struct C3 char b;4 int a;5 short c;6 GNUC_PACKED;     此時(shí)sizeof(struct C)的值為7。3.2 棧內(nèi)存對齊     在VC/C+中,棧的對齊方式不受結(jié)構(gòu)體成員對齊選項(xiàng)的影響。總是保

34、持對齊且對齊在4字節(jié)邊界上。    【例4】 1 #pragma pack(push, 1) /后面可改為1, 2, 4, 8 2 struct StrtE 3 char m1; 4 long m2; 5 ; 6 #pragma pack(pop) 7 8 int main(void) 9 char a;10 short b;11 int c;12 double d2;13 struct StrtE s;14 15 printf("a address: %pn", &a);16 printf("b address: %pn",

35、 &b);17 printf("c address: %pn", &c);18 printf("d0 address: %pn", &(d0);19 printf("d1 address: %pn", &(d1);20 printf("s address: %pn", &s);21 printf("s.m2 address: %pn", &(s.m2);22 return 0;23      結(jié)果如下:1 a addr

36、ess: 0xbfc4cfff2 b address: 0xbfc4cffc3 c address: 0xbfc4cff84 d0 address: 0xbfc4cfe85 d1 address: 0xbfc4cff06 s address: 0xbfc4cfe37 s.m2 address: 0xbfc4cfe4     可以看出都是對齊到4字節(jié)。并且前面的char和short并沒有被湊在一起(成4字節(jié)),這和結(jié)構(gòu)體內(nèi)的處理是不同的。     至于為什么輸出的地址值是變小的,這是因?yàn)樵撈脚_下的棧是倒著“生長”的。3.3 位域?qū)R3.

37、3.1 位域定義     有些信息在存儲時(shí),并不需要占用一個(gè)完整的字節(jié),而只需占幾個(gè)或一個(gè)二進(jìn)制位。例如在存放一個(gè)開關(guān)量時(shí),只有0和1兩種狀態(tài),用一位二進(jìn)位即可。為了節(jié)省存儲空間和處理簡便,C語言提供了一種數(shù)據(jù)結(jié)構(gòu),稱為“位域”或“位段”。     位域是一種特殊的結(jié)構(gòu)成員或聯(lián)合成員(即只能用在結(jié)構(gòu)或聯(lián)合中),用于指定該成員在內(nèi)存存儲時(shí)所占用的位數(shù),從而在機(jī)器內(nèi)更緊湊地表示數(shù)據(jù)。每個(gè)位域有一個(gè)域名,允許在程序中按域名操作對應(yīng)的位。這樣就可用一個(gè)字節(jié)的二進(jìn)制位域來表示幾個(gè)不同的對象。     位域定義與結(jié)構(gòu)

38、定義類似,其形式為:struct 位域結(jié)構(gòu)名        位域列表      其中位域列表的形式為:類型說明符位域名:位域長度     位域的使用和結(jié)構(gòu)成員的使用相同,其一般形式為:位域變量名.位域名     位域允許用各種格式輸出。     位域在本質(zhì)上就是一種結(jié)構(gòu)類型,不過其成員是按二進(jìn)位分配的。位域變量的說明與結(jié)構(gòu)變量說明的方式相同,可先定義后說明、同時(shí)定義說明或直接說明。

39、60;          位域的使用主要為下面兩種情況:     1) 當(dāng)機(jī)器可用內(nèi)存空間較少而使用位域可大量節(jié)省內(nèi)存時(shí)。如把結(jié)構(gòu)作為大數(shù)組的元素時(shí)。     2) 當(dāng)需要把一結(jié)構(gòu)體或聯(lián)合映射成某預(yù)定的組織結(jié)構(gòu)時(shí)。如需要訪問字節(jié)內(nèi)的特定位時(shí)。3.3.2 對齊準(zhǔn)則     位域成員不能單獨(dú)被取sizeof值。下面主要討論含有位域的結(jié)構(gòu)體的sizeof。      C99規(guī)定int、unsigned int和b

40、ool可以作為位域類型,但編譯器幾乎都對此作了擴(kuò)展,允許其它類型的存在。位域作為嵌入式系統(tǒng)中非常常見的一種編程工具,優(yōu)點(diǎn)在于壓縮程序的存儲空間。     其對齊規(guī)則大致為:     1) 如果相鄰位域字段的類型相同,且其位寬之和小于類型的sizeof大小,則后面的字段將緊鄰前一個(gè)字段存儲,直到不能容納為止;     2) 如果相鄰位域字段的類型相同,但其位寬之和大于類型的sizeof大小,則后面的字段將從新的存儲單元開始,其偏移量為其類型大小的整數(shù)倍;     3) 如果相鄰

41、的位域字段的類型不同,則各編譯器的具體實(shí)現(xiàn)有差異,VC6采取不壓縮方式,Dev-C+和GCC采取壓縮方式;     4) 如果位域字段之間穿插著非位域字段,則不進(jìn)行壓縮;     5) 整個(gè)結(jié)構(gòu)體的總大小為最寬基本類型成員大小的整數(shù)倍,而位域則按照其最寬類型字節(jié)數(shù)對齊。    【例5】1 struct BitField2 char element1 : 1;3 char element2 : 4;4 char element3 : 5;5 ;     位域類型為char,第1個(gè)字節(jié)僅能

42、容納下element1和element2,所以element1和element2被壓縮到第1個(gè)字節(jié)中,而element3只能從下一個(gè)字節(jié)開始。因此sizeof(BitField)的結(jié)果為2。    【例6】1 struct BitField12 char element1 : 1;3 short element2 : 5;4 char element3 : 7;5 ;     由于相鄰位域類型不同,在VC6中其sizeof為6,在Dev-C+中為2。    【例7】1 struct BitField22 char eleme

43、nt1 : 3;3 char element2 ;4 char element3 : 5;5 ;     非位域字段穿插在其中,不會產(chǎn)生壓縮,在VC6和Dev-C+中得到的大小均為3。    【例8】1 struct StructBitField2 int element1 : 1;3 int element2 : 5;4 int element3 : 29;5 int element4 : 6;6 char element5 :2;7 char stelement; /在含位域的結(jié)構(gòu)或聯(lián)合中也可同時(shí)說明普通成員8 ;   

44、  位域中最寬類型int的字節(jié)數(shù)為4,因此結(jié)構(gòu)體按4字節(jié)對齊,在VC6中其sizeof為16。3.3.3 注意事項(xiàng)     關(guān)于位域操作有幾點(diǎn)需要注意:     1) 位域的地址不能訪問,因此不允許將&運(yùn)算符用于位域。不能使用指向位域的指針也不能使用位域的數(shù)組(數(shù)組是種特殊指針)。     例如,scanf函數(shù)無法直接向位域中存儲數(shù)據(jù):1 int main(void) 2 struct BitField1 tBit;3 scanf("%d", &tBit.el

45、ement2); /error: cannot take address of bit-field element24 return 0;5      可用scanf函數(shù)將輸入讀入到一個(gè)普通的整型變量中,然后再賦值給tBit.element2。     2) 位域不能作為函數(shù)返回的結(jié)果。     3) 位域以定義的類型為單位,且位域的長度不能夠超過所定義類型的長度。例如定義int a:33是不允許的。     4) 位域可以不指定位域名,但不能訪問無名的位域。

46、60;    位域可以無位域名,只用作填充或調(diào)整位置,占位大小取決于該類型。例如,char :0表示整個(gè)位域向后推一個(gè)字節(jié),即該無名位域后的下一個(gè)位域從下一個(gè)字節(jié)開始存放,同理short :0和int :0分別表示整個(gè)位域向后推兩個(gè)和四個(gè)字節(jié)。     當(dāng)空位域的長度為具體數(shù)值N時(shí)(如int :2),該變量僅用來占位N位。    【例9】1 struct BitField32 char element1 : 3;3 char :6;4 char element3 : 5;5 ;     結(jié)構(gòu)體大

47、小為3。因?yàn)閑lement1占3位,后面要保留6位而char為8位,所以保留的6位只能放到第2個(gè)字節(jié)。同樣element3只能放到第3字節(jié)。1 struct BitField42 char element1 : 3;3 char :0;4 char element3 : 5;5 ;     長度為0的位域告訴編譯器將下一個(gè)位域放在一個(gè)存儲單元的起始位置。如上,編譯器會給成員element1分配3位,接著跳過余下的4位到下一個(gè)存儲單元,然后給成員element3分配5位。故上面的結(jié)構(gòu)體大小為2。     5) 位域的表示范圍。·

48、; 位域的賦值不能超過其可以表示的范圍;· 位域的類型決定該編碼能表示的值的結(jié)果。     對于第二點(diǎn),若位域?yàn)閡nsigned類型,則直接轉(zhuǎn)化為正數(shù);若非unsigned類型,則先判斷最高位是否為1,若為1表示補(bǔ)碼,則對其除符號位外的所有位取反再加一得到最后的結(jié)果數(shù)據(jù)(原碼)。如:1 unsigned int p:3 = 111; /p表示72 int p:3 = 111; /p 表示-1,對除符號位之外的所有位取反再加一     6) 帶位域的結(jié)構(gòu)在內(nèi)存中各個(gè)位域的存儲方式取決于編譯器,既可從左到右也可從右到左存儲。&#

49、160;   【例10】在VC6下執(zhí)行下面的代碼:int main(void) union int i; struct char a : 1; char b : 1; char c : 2; bits; num; printf("Input an integer for i(015): "); scanf("%d", &num.i); printf("i = %d, cba = %d %d %dn", num.i, num.bits.c, num.bits.b, num.bits.a); return 0; 

50、;    輸入i值為11,則輸出為i = 11, cba = -2 -1 -1。     Intel x86處理器按小字節(jié)序存儲數(shù)據(jù),所以bits中的位域在內(nèi)存中放置順序?yàn)閏cba。當(dāng)num.i置為11時(shí),bits的最低有效位(即位域a)的值為1,a、b、c按低地址到高地址分別存儲為10、1、1(二進(jìn)制)。     但為什么最后的打印結(jié)果是a=-1而不是1?     因?yàn)槲挥騛定義的類型signed char是有符號數(shù),所以盡管a只有1位,仍要進(jìn)行符號擴(kuò)展。1做為補(bǔ)碼存在,對應(yīng)原碼-1

51、。     如果將a、b、c的類型定義為unsigned char,即可得到cba = 2 1 1。1011即為11的二進(jìn)制數(shù)。     注:C語言中,不同的成員使用共同的存儲區(qū)域的數(shù)據(jù)構(gòu)造類型稱為聯(lián)合(或共用體)。聯(lián)合占用空間的大小取決于類型長度最大的成員。聯(lián)合在定義、說明和使用形式上與結(jié)構(gòu)體相似。      7) 位域的實(shí)現(xiàn)會因編譯器的不同而不同,使用位域會影響程序可移植性。因此除非必要否則最好不要使用位域。     8) 盡管使用位域可以節(jié)省內(nèi)存空間,但卻增加

52、了處理時(shí)間。當(dāng)訪問各個(gè)位域成員時(shí),需要把位域從它所在的字中分解出來或反過來把一值壓縮存到位域所在的字位中。 四  總結(jié)     讓我們回到引言部分的問題。     缺省情況下,C/C+編譯器默認(rèn)將結(jié)構(gòu)、棧中的成員數(shù)據(jù)進(jìn)行內(nèi)存對齊。因此,引言程序輸出就變成"c1 -> 0, s -> 2, c2 -> 4, i -> 8"。     編譯器將未對齊的成員向后移,將每一個(gè)都成員對齊到自然邊界上,從而也導(dǎo)致整個(gè)結(jié)構(gòu)的尺寸變大。盡管會犧牲一點(diǎn)空間(成

53、員之間有空洞),但提高了性能。     也正是這個(gè)原因,引言例子中sizeof(T_ FOO)為12,而不是8。      總結(jié)說來,就是在結(jié)構(gòu)體中,綜合考慮變量本身和指定的對齊值;在棧上,不考慮變量本身的大小,統(tǒng)一對齊到4字節(jié)。 五  附錄5.1 字節(jié)序與網(wǎng)絡(luò)序5.1.1 字節(jié)序     字節(jié)序,顧名思義就是字節(jié)的高低位存放順序。     對于單字節(jié),大部分處理器以相同的順序處理比特位,因此單字節(jié)的存放和傳輸方式一般相同。  

54、0;  對于多字節(jié)數(shù)據(jù),如整型(32位機(jī)中一般占4字節(jié)),在不同的處理器的存放方式主要有兩種(以內(nèi)存中0x0A0B0C0D的存放方式為例)。     1) 大字節(jié)序(Big-Endian,又稱大端序或大尾序)     在計(jì)算機(jī)中,存儲介質(zhì)以下面方式存儲整數(shù)0x0A0B0C0D則稱為大字節(jié)序:數(shù)據(jù)以8bit為單位低地址方向0x0A0x0B0x0C0x0D高地址方向數(shù)據(jù)以16bit為單位低地址方向0x0A0B0x0C0D高地址方向     其中,最高有效位(MSB,Most Significant

55、Byte)0x0A存儲在最低的內(nèi)存地址處。下個(gè)字節(jié)0x0B存在后面的地址處。同時(shí),最高的16bit單元0x0A0B存儲在低位。     簡而言之,大字節(jié)序就是“高字節(jié)存入低地址,低字節(jié)存入高地址”。     這里講個(gè)詞源典故:“endian”一詞來源于喬納森·斯威夫特的小說格列佛游記。小說中,小人國為水煮蛋該從大的一端(Big-End)剝開還是小的一端(Little-End)剝開而爭論,爭論的雙方分別被稱為Big-endians和Little-endians。     1980年,Danny C

56、ohen在其著名的論文"On Holy Wars and a Plea for Peace"中為平息一場關(guān)于字節(jié)該以什么樣的順序傳送的爭論而引用了該詞。     借用上面的典故,想象一下要把熟雞蛋旋轉(zhuǎn)著穩(wěn)立起來,大頭(高字節(jié))肯定在下面(低地址)_     2) 小字節(jié)序(Little-Endian,又稱小端序或小尾序)     在計(jì)算機(jī)中,存儲介質(zhì)以下面方式存儲整數(shù)0x0A0B0C0D則稱為小字節(jié)序:數(shù)據(jù)以8bit為單位高地址方向0x0A0x0B0x0C0x0D低地址方向數(shù)據(jù)以16b

57、it為單位高地址方向0x0A0B0x0C0D低地址方向     其中,最低有效位(LSB,Least Significant Byte)0x0D存儲在最低的內(nèi)存地址處。后面字節(jié)依次存在后面的地址處。同時(shí),最低的16bit單元0x0A0B存儲在低位。     可見,小字節(jié)序就是“高字節(jié)存入高地址,低字節(jié)存入低地址”。      C語言中的位域結(jié)構(gòu)也要遵循比特序(類似字節(jié)序)。例如:1 struct bitfield2 unsigned char a: 2;3 unsigned char b: 6;

58、4      該位域結(jié)構(gòu)占1個(gè)字節(jié),假設(shè)賦值a = 0x01和b=0x02,則大字節(jié)機(jī)器上該字節(jié)為(01)(000010),小字節(jié)機(jī)器上該字節(jié)為(000010)(01)。因此在編寫可移植代碼時(shí),需要加條件編譯。     注意,在包含位域的C結(jié)構(gòu)中,若位域A在位域B之前定義,則位域A所占用的內(nèi)存空間地址低于位域B所占用的內(nèi)存空間。     對上述問題,詳細(xì)的講解可參考     另見以下聯(lián)合體,在小字節(jié)機(jī)器上若low=0x01,high=0x02,則hex=0x21: 1 int

59、 main(void) 2 union 3 unsigned char hex; 4 struct 5 unsigned char low : 4; 6 unsigned char high : 4; 7 ; 8 convert; 9 convert.low = 0x01;10 convert.high = 0x02;11 printf("hex = 0x%0xn", convert.hex);12 return 0;13 5.1.2 網(wǎng)絡(luò)序     網(wǎng)絡(luò)傳輸一般采用大字節(jié)序,也稱為網(wǎng)絡(luò)字節(jié)序或網(wǎng)絡(luò)序。IP協(xié)議中定義大字節(jié)序?yàn)榫W(wǎng)絡(luò)字節(jié)序。

60、60;    對于可移植的代碼來說,將接收的網(wǎng)絡(luò)數(shù)據(jù)轉(zhuǎn)換成主機(jī)的字節(jié)序是必須的,一般會有成對的函數(shù)用于把網(wǎng)絡(luò)數(shù)據(jù)轉(zhuǎn)換成相應(yīng)的主機(jī)字節(jié)序或反之(若主機(jī)字節(jié)序與網(wǎng)絡(luò)字節(jié)序相同,通常將函數(shù)定義為空宏)。     伯克利socket API定義了一組轉(zhuǎn)換函數(shù),用于16和32位整數(shù)在網(wǎng)絡(luò)序和主機(jī)字節(jié)序之間的轉(zhuǎn)換。Htonl、htons用于主機(jī)序轉(zhuǎn)換到網(wǎng)絡(luò)序;ntohl、ntohs用于網(wǎng)絡(luò)序轉(zhuǎn)換到本機(jī)序。     注意:在大小字節(jié)序轉(zhuǎn)換時(shí),必須考慮待轉(zhuǎn)換數(shù)據(jù)的長度(如5.1.1節(jié)的數(shù)據(jù)單元)。另外對于單字符或小于單字符的幾個(gè)b

61、it數(shù)據(jù),是不必轉(zhuǎn)換的,因?yàn)樵跈C(jī)器存儲和網(wǎng)絡(luò)發(fā)送的一個(gè)字符內(nèi)的bit位存儲順序是一致的。5.1.3 位序     用于描述串行設(shè)備的傳輸順序。一般硬件傳輸采用小字節(jié)序(先傳低位),但I(xiàn)2C協(xié)議采用大字節(jié)序。網(wǎng)絡(luò)協(xié)議中只有數(shù)據(jù)鏈路層的底端會涉及到。5.1.4 處理器字節(jié)序     不同處理器體系的字節(jié)序如下所示:· X86、MOS Technology 6502、Z80、VAX、PDP-11等處理器為Little endian;· Motorola 6800、Motorola 68000、PowerPC 970、Sy

62、stem/370、SPARC(除V9外)等處理器為Big endian;· ARM、PowerPC (除PowerPC 970外)、DEC Alpha,SPARC V9,MIPS,PA-RISC and IA64等的字節(jié)序是可配置的。5.1.5 字節(jié)序編程     請看下面的語句:1 printf("%cn", *(short*)"AB") >> 8);     在大字節(jié)序下輸出為A,小字節(jié)序下輸出為B。     下面的代碼可用來判斷本地機(jī)器字

63、節(jié)序: 1 /字節(jié)序枚舉類型 2 typedef enum 3 ENDIAN_LITTLE = (INT8U)0X00, 4 ENDIAN_BIG = (INT8U)0X01 5 E_ENDIAN_TYPE; 6 7 E_ENDIAN_TYPE GetEndianType(VOID) 8 9 INT32U dwData = 0x12345678;10 11 if(0x78 = *(INT8U*)&dwData)12 return ENDIAN_LITTLE;13 else14 return ENDIAN_BIG;15 16 17 /Start of GetEndianTypeTest/

64、18 #include <endian.h>19 VOID GetEndianTypeTest(VOID)20 21 #if _BYTE_ORDER = _LITTLE_ENDIAN22 printf("%s<Test Case> Result: %s, EndianType = %s!n", _FUNCTION_, 23 (ENDIAN_LITTLE != GetEndianType() ? "ERROR" : "OK", "Little");24 #elif _BYTE_ORDER = _BIG_ENDIAN25 printf("%s<Test Case> Result: %s, EndianType = %s!n", _FUNCTION_, 26 (ENDIAN_BIG != GetEndianType() ? "ERROR" : "OK", "Big");27

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論