http://www.cnblogs.com/cbscan/articles/2033138.html 對于內(nèi)存要求高的程序,,要有效控制數(shù)據(jù)結(jié)構(gòu)的大小,,熟悉對齊規(guī)則是必要的。在開始寫一個(gè)結(jié)構(gòu)體之前,,按照長度大小順序來寫成員是一個(gè)需要記住的技巧,。 許多實(shí)際的計(jì)算機(jī)系統(tǒng)對基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會要求這些數(shù)據(jù)的首地址的值是某個(gè)數(shù)k(通常它為4或8)的倍數(shù),,這就是所謂的內(nèi)存對齊,,而這個(gè)k則被稱為該數(shù)據(jù)類型的對齊模數(shù)(alignment modulus)。當(dāng)一種類型S的對齊模數(shù)與另一種類型T的對齊模數(shù)的比值是大于1的整數(shù),我們就稱類型S的對齊要求比T強(qiáng)(嚴(yán)格),,而稱T比S弱(寬松),。這種強(qiáng)制的要求一來簡化了處理器與內(nèi)存之間傳輸系統(tǒng)的設(shè)計(jì),二來可以提升讀取數(shù)據(jù)的速度,。比如這么一種處理器,,它每次讀寫內(nèi)存的時(shí)候都從某個(gè)8倍數(shù)的地址開始,一次讀出或?qū)懭?個(gè)字節(jié)的數(shù)據(jù),,假如軟件能保證double類型的數(shù)據(jù)都從8倍數(shù)地址開始,,那么讀或?qū)懸粋€(gè)double類型數(shù)據(jù)就只需要一次內(nèi)存操作。否則,,我們就可能需要兩次內(nèi)存操作才能完成這個(gè)動作,,因?yàn)閿?shù)據(jù)或許恰好橫跨在兩個(gè)符合對齊要求的8字節(jié)內(nèi)存塊上。某些處理器在數(shù)據(jù)不滿足對齊要求的情況下可能會出錯(cuò),,但是Intel的IA32架構(gòu)的處理器則不管數(shù)據(jù)是否對齊都能正確工作,。不過Intel奉勸大家,如果想提升性能,,那么所有的程序數(shù)據(jù)都應(yīng)該盡可能地對齊,。 ANSI C標(biāo)準(zhǔn)中并沒有規(guī)定,相鄰聲明的變量在內(nèi)存中一定要相鄰,。為了程序的高效性,,內(nèi)存對齊問題由編譯器自行靈活處理,這樣導(dǎo)致相鄰的變量之間可能會有一些填充字節(jié),。對于基本數(shù)據(jù)類型(int char),,他們占用的內(nèi)存空間在一個(gè)確定硬件系統(tǒng)下有個(gè)確定的值,所以,,接下來我們只是考慮結(jié)構(gòu)體成員內(nèi)存分配情況,。 struct T { char ch; int i ; }; Win32平臺下的微軟C編譯器(cl.exe for 80×86)的對齊策略: 1) 結(jié)構(gòu)體變量的首地址能夠被其最寬基本類型成員的大小所整除; 備注:編譯器在給結(jié)構(gòu)體開辟空間時(shí),,首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類型,,然后尋找內(nèi)存地址能被該基本數(shù)據(jù)類型所整除的位置,作為結(jié)構(gòu)體的首地址,。將這個(gè)最寬的基本數(shù)據(jù)類型的大小作為上面介紹的對齊模數(shù),。 2) 結(jié)構(gòu)體每個(gè)成員相對于結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍,如有需要編譯器會在成員之間加上填充字節(jié)(internal adding),; 備注:為結(jié)構(gòu)體的一個(gè)成員開辟空間之前,,編譯器首先檢查預(yù)開辟空間的首地址相對于結(jié)構(gòu)體首地址的偏移是否是本成員的整數(shù)倍,若是,,則存放本成員,,反之,,則在本成員和上一個(gè)成員之間填充一定的字節(jié),以達(dá)到整數(shù)倍的要求,,也就是將預(yù)開辟空間的首地址后移幾個(gè)字節(jié),。 3) 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,如有需要,,編譯器會在最末一個(gè)成員之后加上填充字節(jié)(trailing padding),。 備注:結(jié)構(gòu)體總大小是包括填充字節(jié),最后一個(gè)成員滿足上面兩條以外,,還必須滿足第三條,,否則就必須在最后填充幾個(gè)字節(jié)以達(dá)到本條要求。 根據(jù)以上準(zhǔn)則,,在windows下,,使用VC編譯器,sizeof(T)的大小為8個(gè)字節(jié),。 而在GNU GCC編譯器中,,遵循的準(zhǔn)則有些區(qū)別,對齊模數(shù)不是像上面所述的那樣,,根據(jù)最寬的基本數(shù)據(jù)類型來定,。在GCC中,對齊模數(shù)的準(zhǔn)則是:對齊模數(shù)最大只能是4,,也就是說,,即使結(jié)構(gòu)體中有double類型,對齊模數(shù)還是4,,所以對齊模數(shù)只能是1,,2,4,。而且在上述的三條中,,第2條里,offset必須是成員大小的整數(shù)倍,,如果這個(gè)成員大小小于等于4則按照上述準(zhǔn)則進(jìn)行,,但是如果大于4了,則結(jié)構(gòu)體每個(gè)成員相對于結(jié)構(gòu)體首地址的偏移量(offset)只能按照是4的整數(shù)倍來進(jìn)行判斷是否添加填充,。最后的大小來按照4來判斷,。 看如下例子:struct T { char ch; double d ; }; 那么在GCC下,,sizeof(T)應(yīng)該等于12個(gè)字節(jié),。 如果結(jié)構(gòu)體中含有位域(bit-field),那么VC中準(zhǔn)則又要有所更改: 1) 如果相鄰位域字段的類型相同,,且其位寬之和小于類型的sizeof大小,,則后面的字段將緊鄰前一個(gè)字段存儲,,直到不能容納為止; 2) 如果相鄰位域字段的類型相同,,但其位寬之和大于類型的sizeof大小,,則后面的字段將從新的存儲單元開始,其偏移量為其類型大小的整數(shù)倍,; 3) 如果相鄰的位域字段的類型不同,,則各編譯器的具體實(shí)現(xiàn)有差異,VC6采取不壓縮方式(不同位域字段存放在不同的位域類型字節(jié)中),,Dev-C++和GCC都采取壓縮方式,; 備注:當(dāng)兩字段類型不一樣的時(shí)候,對于不壓縮方式,,例如:struct N { char c:2; int i:4; }; 依然要滿足不含位域結(jié)構(gòu)體內(nèi)存對齊準(zhǔn)則第2條,,i成員相對于結(jié)構(gòu)體首地址的偏移應(yīng)該是4的整數(shù)倍,所以c成員后要填充3個(gè)字節(jié),,然后再開辟4個(gè)字節(jié)的空間作為int型,,其中4位用來存放i,所以上面結(jié)構(gòu)體在VC中所占空間為8個(gè)字節(jié),;而對于采用壓縮方式的編譯器來說,,遵循不含位域結(jié)構(gòu)體內(nèi)存對齊準(zhǔn)則第2條,不同的是,,如果填充的3個(gè)字節(jié)能容納后面成員的位,,則壓縮到填充字節(jié)中,不能容納,,則要單獨(dú)開辟空間,,所以上面結(jié)構(gòu)體N在GCC或者Dev-C++中所占空間應(yīng)該是4個(gè)字節(jié)。 4) 如果位域字段之間穿插著非位域字段,,則不進(jìn)行壓縮,; 備注: 結(jié)構(gòu)體typedef struct { char c:2; double i; int c2:4; }N3; 在GCC下占據(jù)的空間為16字節(jié),在VC下占據(jù)的空間應(yīng)該是24個(gè)字節(jié),。 ps:對齊模數(shù)的選擇只能是根據(jù)基本數(shù)據(jù)類型,,所以對于結(jié)構(gòu)體中嵌套結(jié)構(gòu)體,只能考慮其拆分的基本數(shù)據(jù)類型,。(而對于對齊準(zhǔn)則中的第2條,,確是要將整個(gè)結(jié)構(gòu)體看成是一個(gè)成員,成員大小按照該結(jié)構(gòu)體根據(jù)對齊準(zhǔn)則判斷所得的大小,。--不解這句話,?據(jù)程序結(jié)果來看,無論是成員對齊還是最后的總大小填充,,都是以最嚴(yán)格基本類型來判斷的)類對象在內(nèi)存中存放的方式和結(jié)構(gòu)體類似,,這里就不再說明,。需要指出的是,類對象的大小只是包括類中非靜態(tài)成員變量所占的空間,,如果有虛函數(shù),,那么再另外增加一個(gè)指針?biāo)嫉目臻g即可。 struct test { char a; short b; double c; }; struct test2 { char a; struct test t; }; int main() { struct test2 t; printf("%d\n", sizeof(t)); } VC將輸出24,,而gcc將輸出16,。還有#pragma pack()的一點(diǎn)補(bǔ)充 自身對齊值:即數(shù)據(jù)類型的自身的對齊值。例如char型的數(shù)據(jù),,其自身對齊值為1字節(jié);short型的數(shù)據(jù),,其自身對齊值為2字節(jié);int,float,long類型,其自身對齊值為4字節(jié);double類型,,其自身對齊值為4字節(jié);而struct和class類型的數(shù)據(jù)其自身對齊值為其成員變量中自身對齊值最大的那個(gè)值,。 指定對齊值:#pragma pack (value)時(shí)指定的對齊值value 有效對齊值:上述兩個(gè)對齊值中最小的那個(gè)。 struct A { char c; int i; short s; }; #pragma pack (2) /* 指定按2字節(jié)對齊 */ struct B { char c; short s; int i; }; #pragma pack () /* 恢復(fù)默認(rèn)對齊 */#pragma pack(push)和#pragma pack(pop)的意義是顯然的,。 #pragma pack(n) 至于編譯器的對齊選項(xiàng),,忽略不計(jì)。 為了防止不同編譯器對齊不一樣,,建議在代碼里面指定對齊參數(shù) 可能重要的一點(diǎn)是關(guān)于緊縮結(jié)構(gòu)的,。緊縮結(jié)構(gòu)的用途 其實(shí)最常用的結(jié)構(gòu)對齊選項(xiàng)就是:默認(rèn)對齊和緊縮。在兩個(gè)程序,,或者兩個(gè)平臺之間傳遞數(shù)據(jù)時(shí),,我們通常會將數(shù)據(jù)結(jié)構(gòu)設(shè)置為緊縮的。這樣不僅可以減小通信量,,還可以避免對齊帶來的麻煩,。假設(shè)甲乙雙方進(jìn)行跨平臺通信,甲方使用了“/Zp2”這么奇怪的對齊選項(xiàng),,而乙方的編譯器不支持這種對齊方式,,那么乙方就可以理解什么叫欲哭無淚了。 當(dāng)我們需要一個(gè)字節(jié)一個(gè)字節(jié)訪問結(jié)構(gòu)數(shù)據(jù)時(shí),,我們通常都會希望結(jié)構(gòu)是緊縮的,,這樣就不必考慮哪個(gè)字節(jié)是填充字節(jié)了。我們把數(shù)據(jù)保存到非易失設(shè)備時(shí),,通常也會采用緊縮結(jié)構(gòu),,既減小存儲量,也方便其它程序讀出,。 各編譯器都支持結(jié)構(gòu)的緊縮,,即連續(xù)排列結(jié)構(gòu)的各成員變量,各成員變量之間沒有任何填充字節(jié)。這時(shí),,結(jié)構(gòu)的大小等于各成員變量大小的和。緊縮結(jié)構(gòu)的變量可以放在1n邊界,,即任意地址邊界,。在GNU gcc: typedef struct St2Tag { St1 st1; char ch2; } __attribute__ ((packed)) St2; 在ARMCC: typedef __packed struct St2Tag { St1 st1; char ch2; } St2; 在VC: #pragma pack(1) typedef struct St2Tag { St1 st1; char ch2; } St2; #pragma pack() 針對不同的編譯器: #ifdef __GNUC__ #define GNUC_PACKED __attribute__ ((packed)) #else #define GNUC_PACKED #endif #ifdef __arm #define ARM_PACKED __packed #else #define ARM_PACKED #endif #ifdef WIN32 #pragma pack(1) #endif typedef ARM_PACKED struct St2Tag { St1 st1; char ch2; } GNUC_PACKED St2; #ifdef WIN32 #pragma pack() #endif 最后記錄一個(gè)小細(xì)節(jié)。gcc編譯器和VC編譯器都支持在緊縮結(jié)構(gòu)中包含非緊縮結(jié)構(gòu),,例如前面例子中的St2可以包含非緊縮的St1,。但對于ARM編譯器而言,緊縮結(jié)構(gòu)包含的其它結(jié)構(gòu)必須是緊縮的,。如果緊縮的St2包含了非緊縮的St1,,編譯時(shí)就會報(bào)錯(cuò)。 |
|