C/C++編程筆記:C語(yǔ)言對(duì)齊問(wèn)題【結(jié)構(gòu)體、棧內(nèi)存以及位域?qū)R】

引言
考慮下面的結(jié)構(gòu)體定義:

假設(shè)這個(gè)結(jié)構(gòu)體的成員在內(nèi)存中是緊湊排列的,且c1的起始地址是0,則s的地址就是1,c2的地址是3,i的地址是4。
現(xiàn)在,我們編寫(xiě)一個(gè)簡(jiǎn)單的程序:

運(yùn)行后輸出:

為什么會(huì)這樣?這就是字節(jié)對(duì)齊導(dǎo)致的問(wèn)題。
本文在參考諸多資料的基礎(chǔ)上,詳細(xì)介紹常見(jiàn)的字節(jié)對(duì)齊問(wèn)題。因成文較早,資料來(lái)源大多已不可考,敬請(qǐng)諒解。
一,什么是字節(jié)對(duì)齊
現(xiàn)代計(jì)算機(jī)中,內(nèi)存空間按照字節(jié)劃分,理論上可以從任何起始地址訪(fǎng)問(wèn)任意類(lèi)型的變量。但實(shí)際中在訪(fǎng)問(wèn)特定類(lèi)型變量時(shí)經(jīng)常在特定的內(nèi)存地址訪(fǎng)問(wèn),這就需要各種類(lèi)型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序一個(gè)接一個(gè)地存放,這就是對(duì)齊。
二,對(duì)齊的原因和作用
不同硬件平臺(tái)對(duì)存儲(chǔ)空間的處理上存在很大的不同。某些平臺(tái)對(duì)特定類(lèi)型的數(shù)據(jù)只能從特定地址開(kāi)始存取,而不允許其在內(nèi)存中任意存放。例如Motorola 68000處理器不允許16位的字存放在奇地址,否則會(huì)觸發(fā)異常,因此在這種架構(gòu)下編程必須保證字節(jié)對(duì)齊。
但最常見(jiàn)的情況是,如果不按照平臺(tái)要求對(duì)數(shù)據(jù)存放進(jìn)行對(duì)齊,會(huì)帶來(lái)存取效率上的損失。比如32位的Intel處理器通過(guò)總線(xiàn)訪(fǎng)問(wèn)(包括讀和寫(xiě))內(nèi)存數(shù)據(jù)。每個(gè)總線(xiàn)周期從偶地址開(kāi)始訪(fǎng)問(wèn)32位內(nèi)存數(shù)據(jù),內(nèi)存數(shù)據(jù)以字節(jié)為單位存放。如果一個(gè)32位的數(shù)據(jù)沒(méi)有存放在4字節(jié)整除的內(nèi)存地址處,那么處理器就需要2個(gè)總線(xiàn)周期對(duì)其進(jìn)行訪(fǎng)問(wèn),顯然訪(fǎng)問(wèn)效率下降很多。
因此,通過(guò)合理的內(nèi)存對(duì)齊可以提高訪(fǎng)問(wèn)效率。為使CPU能夠?qū)?shù)據(jù)進(jìn)行快速訪(fǎng)問(wèn),數(shù)據(jù)的起始地址應(yīng)具有“對(duì)齊”特性。比如4字節(jié)數(shù)據(jù)的起始地址應(yīng)位于4字節(jié)邊界上,即起始地址能夠被4整除。
此外,合理利用字節(jié)對(duì)齊還可以有效地節(jié)省存儲(chǔ)空間。但要注意,在32位機(jī)中使用1字節(jié)或2字節(jié)對(duì)齊,反而會(huì)降低變量訪(fǎng)問(wèn)速度。因此需要考慮處理器類(lèi)型。還應(yīng)考慮編譯器的類(lèi)型。在VC/C++和GNU GCC中都是默認(rèn)是4字節(jié)對(duì)齊。
三,對(duì)齊的分類(lèi)和準(zhǔn)則
主要基于Intel X86架構(gòu)介紹結(jié)構(gòu)體對(duì)齊和棧內(nèi)存對(duì)齊,位域本質(zhì)上為結(jié)構(gòu)體類(lèi)型。
對(duì)于Intel X86平臺(tái),每次分配內(nèi)存應(yīng)該是從4的整數(shù)倍地址開(kāi)始分配,無(wú)論是對(duì)結(jié)構(gòu)體變量還是簡(jiǎn)單類(lèi)型的變量。
3.1 結(jié)構(gòu)體對(duì)齊
在C語(yǔ)言中,結(jié)構(gòu)體是種復(fù)合數(shù)據(jù)類(lèi)型,其構(gòu)成元素既可以是基本數(shù)據(jù)類(lèi)型(如int、long、float等)的變量,也可以是一些復(fù)合數(shù)據(jù)類(lèi)型(如數(shù)組、結(jié)構(gòu)體、聯(lián)合等)的數(shù)據(jù)單元。編譯器為結(jié)構(gòu)體的每個(gè)成員按照其自然邊界(alignment)分配空間。各成員按照它們被聲明的順序在內(nèi)存中順序存儲(chǔ),第一個(gè)成員的地址和整個(gè)結(jié)構(gòu)的地址相同。
字節(jié)對(duì)齊的問(wèn)題主要就是針對(duì)結(jié)構(gòu)體。
3.1.1 簡(jiǎn)單示例
先看個(gè)簡(jiǎn)單的例子(32位,X86處理器,GCC編譯器):
【例1】設(shè)結(jié)構(gòu)體如下定義:

已知32位機(jī)器上各數(shù)據(jù)類(lèi)型的長(zhǎng)度為: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也一樣。按理說(shuō)A和B大小應(yīng)該都是7字節(jié)。之所以出現(xiàn)上述結(jié)果,就是因?yàn)榫幾g器要對(duì)數(shù)據(jù)成員在空間上進(jìn)行對(duì)齊。
3.1.2 對(duì)齊準(zhǔn)則
先來(lái)看四個(gè)重要的基本概念:
(1)數(shù)據(jù)類(lèi)型自身的對(duì)齊值:char型數(shù)據(jù)自身對(duì)齊值為1字節(jié),short型數(shù)據(jù)為2字節(jié),int/float型為4字節(jié),double型為8字節(jié)。
(2)結(jié)構(gòu)體或類(lèi)的自身對(duì)齊值:其成員中自身對(duì)齊值最大的那個(gè)值。
(3)指定對(duì)齊值:#pragma pack (value)時(shí)的指定對(duì)齊值value。
(4)數(shù)據(jù)成員、結(jié)構(gòu)體和類(lèi)的有效對(duì)齊值:自身對(duì)齊值和指定對(duì)齊值中較小者,即有效對(duì)齊值=min{自身對(duì)齊值,當(dāng)前指定的pack值}。
基于上面這些值,就可以方便地討論具體數(shù)據(jù)結(jié)構(gòu)的成員和其自身的對(duì)齊方式。
其中,有效對(duì)齊值N是最終用來(lái)決定數(shù)據(jù)存放地址方式的值。有效對(duì)齊N表示“對(duì)齊在N上”,即該數(shù)據(jù)的“存放起始地址%N=0”。而數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)變量都是按定義的先后順序存放。第一個(gè)數(shù)據(jù)變量的起始地址就是數(shù)據(jù)結(jié)構(gòu)的起始地址。結(jié)構(gòu)體的成員變量要對(duì)齊存放,結(jié)構(gòu)體本身也要根據(jù)自身的有效對(duì)齊值圓整(即結(jié)構(gòu)體成員變量占用總長(zhǎng)度為結(jié)構(gòu)體有效對(duì)齊值的整數(shù)倍)。
以此分析3.1.1節(jié)中的結(jié)構(gòu)體B:
假設(shè)B從地址空間0x0000開(kāi)始存放,且指定對(duì)齊值默認(rèn)為4(4字節(jié)對(duì)齊)。成員變量b的自身對(duì)齊值是1,比默認(rèn)指定對(duì)齊值4小,所以其有效對(duì)齊值為1,其存放地址0x0000符合0x0000%1=0。成員變量a自身對(duì)齊值為4,所以有效對(duì)齊值也為4,只能存放在起始地址為0x0004~0x0007四個(gè)連續(xù)的字節(jié)空間中,符合0x0004%4=0且緊靠第一個(gè)變量。變量c自身對(duì)齊值為2,所以有效對(duì)齊值也是2,可存放在0x0008~0x0009兩個(gè)字節(jié)空間中,符合0x0008%2=0。所以從0x0000~0x0009存放的都是B內(nèi)容。
再看數(shù)據(jù)結(jié)構(gòu)B的自身對(duì)齊值為其變量中最大對(duì)齊值(這里是b)所以就是4,所以結(jié)構(gòu)體的有效對(duì)齊值也是4。根據(jù)結(jié)構(gòu)體圓整的要求,0x0000~0x0009=10字節(jié),(10+2)%4=0。所以0x0000A~0x000B也為結(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沒(méi)有問(wèn)題,但是第二個(gè)結(jié)構(gòu)呢?按照數(shù)組的定義,數(shù)組中所有元素都緊挨著。如果我們不把結(jié)構(gòu)體大小補(bǔ)充為4的整數(shù)倍,那么下一個(gè)結(jié)構(gòu)的起始地址將是0x0000A,這顯然不能滿(mǎn)足結(jié)構(gòu)的地址對(duì)齊。因此要把結(jié)構(gòu)體補(bǔ)充成有效對(duì)齊大小的整數(shù)倍。其實(shí)對(duì)于char/short/int/float/double等已有類(lèi)型的自身對(duì)齊值也是基于數(shù)組考慮的,只是因?yàn)檫@些類(lèi)型的長(zhǎng)度已知,所以他們的自身對(duì)齊值也就已知。
上面的概念非常便于理解,不過(guò)個(gè)人還是更喜歡下面的對(duì)齊準(zhǔn)則。
結(jié)構(gòu)體字節(jié)對(duì)齊的細(xì)節(jié)和具體編譯器實(shí)現(xiàn)相關(guān),但一般而言滿(mǎn)足三個(gè)準(zhǔn)則:
(1)結(jié)構(gòu)體變量的首地址能夠被其最寬基本類(lèi)型成員的大小所整除;
(2)結(jié)構(gòu)體每個(gè)成員相對(duì)結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍,如有需要編譯器會(huì)在成員之間加上填充字節(jié)(internal adding);
(3)結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類(lèi)型成員大小的整數(shù)倍,如有需要編譯器會(huì)在最末一個(gè)成員之后加上填充字節(jié){trailing padding}。
對(duì)于以上規(guī)則的說(shuō)明如下:
(1)編譯器在給結(jié)構(gòu)體開(kāi)辟空間時(shí),首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類(lèi)型,然后尋找內(nèi)存地址能被該基本數(shù)據(jù)類(lèi)型所整除的位置,作為結(jié)構(gòu)體的首地址。將這個(gè)最寬的基本數(shù)據(jù)類(lèi)型的大小作為上面介紹的對(duì)齊模數(shù)。
(2)為結(jié)構(gòu)體的一個(gè)成員開(kāi)辟空間之前,編譯器首先檢查預(yù)開(kāi)辟空間的首地址相對(duì)于結(jié)構(gòu)體首地址的偏移是否是本成員大小的整數(shù)倍,若是,則存放本成員,反之,則在本成員和上一個(gè)成員之間填充一定的字節(jié),以達(dá)到整數(shù)倍的要求,也就是將預(yù)開(kāi)辟空間的首地址后移幾個(gè)字節(jié)。
(3)結(jié)構(gòu)體總大小是包括填充字節(jié),最后一個(gè)成員滿(mǎn)足上面兩條以外,還必須滿(mǎn)足第三條,否則就必須在最后填充幾個(gè)字節(jié)以達(dá)到本條要求。
【例2】假設(shè)4字節(jié)對(duì)齊,以下程序的輸出結(jié)果是多少?

執(zhí)行后輸出如下:

下面來(lái)具體分析:
首先char a占用1個(gè)字節(jié),沒(méi)問(wèn)題。
short b本身占用2個(gè)字節(jié),根據(jù)上面準(zhǔn)則2,需要在b和a之間填充1個(gè)字節(jié)。
char c占用1個(gè)字節(jié),沒(méi)問(wèn)題。
int d本身占用4個(gè)字節(jié),根據(jù)準(zhǔn)則2,需要在d和c之間填充3個(gè)字節(jié)。
char e[3];本身占用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 對(duì)齊的隱患
3.1.3.1 數(shù)據(jù)類(lèi)型轉(zhuǎn)換
代碼中關(guān)于對(duì)齊的隱患,很多是隱式的。例如,在強(qiáng)制類(lèi)型轉(zhuǎn)換的時(shí)候:

最后兩句代碼,從奇數(shù)邊界去訪(fǎng)問(wèn)unsigned short型變量,顯然不符合對(duì)齊的規(guī)定。在X86上,類(lèi)似的操作只會(huì)影響效率;但在MIPS或者SPARC上可能導(dǎo)致error,因?yàn)樗鼈円蟊仨氉止?jié)對(duì)齊。
又如對(duì)于3.1.1節(jié)的結(jié)構(gòu)體struct B,定義如下函數(shù):

在函數(shù)體內(nèi)如果直接訪(fǎng)問(wèn)p->a,則很可能會(huì)異常。因?yàn)镸IPS認(rèn)為a是int,其地址應(yīng)該是4的倍數(shù),但p->a的地址很可能不是4的倍數(shù)。
如果p的地址不在對(duì)齊邊界上就可能出問(wèn)題,比如p來(lái)自一個(gè)跨CPU的數(shù)據(jù)包(多種數(shù)據(jù)類(lèi)型的數(shù)據(jù)被按順序放置在一個(gè)數(shù)據(jù)包中傳輸),或p是經(jīng)過(guò)指針移位算出來(lái)的。因此要特別注意跨CPU數(shù)據(jù)的接口函數(shù)對(duì)接口輸入數(shù)據(jù)的處理,以及指針移位再?gòu)?qiáng)制轉(zhuǎn)換為結(jié)構(gòu)指針進(jìn)行訪(fǎng)問(wèn)時(shí)的安全性。
解決方式如下:
定義一個(gè)此結(jié)構(gòu)的局部變量,用memmove方式將數(shù)據(jù)拷貝進(jìn)來(lái)。

注意:如果能確定p的起始地址沒(méi)問(wèn)題,則不需要這么處理;如果不能確定(比如跨CPU輸入數(shù)據(jù)、或指針移位運(yùn)算出來(lái)的數(shù)據(jù)要特別小心),則需要這樣處理。
用#pragma pack (1)將STRUCT_T定義為1字節(jié)對(duì)齊方式。
3.1.3.2 處理器間數(shù)據(jù)通信
處理器間通過(guò)消息(對(duì)于C/C++而言就是結(jié)構(gòu)體)進(jìn)行通信時(shí),需要注意字節(jié)對(duì)齊以及字節(jié)序的問(wèn)題。
大多數(shù)編譯器提供內(nèi)存對(duì)其的選項(xiàng)供用戶(hù)使用。這樣用戶(hù)可以根據(jù)處理器的情況選擇不同的字節(jié)對(duì)齊方式。例如C/C++編譯器提供的#pragma pack(n) n=1,2,4等,讓編譯器在生成目標(biāo)文件時(shí),使內(nèi)存數(shù)據(jù)按照指定的方式排布在1,2,4等字節(jié)整除的內(nèi)存地址處。
然而在不同編譯平臺(tái)或處理器上,字節(jié)對(duì)齊會(huì)造成消息結(jié)構(gòu)長(zhǎng)度的變化。編譯器為了使字節(jié)對(duì)齊可能會(huì)對(duì)消息結(jié)構(gòu)體進(jìn)行填充,不同編譯平臺(tái)可能填充為不同的形式,大大增加處理器間數(shù)據(jù)通信的風(fēng)險(xiǎn)。
下面以32位處理器為例,提出一種內(nèi)存對(duì)齊方法以解決上述問(wèn)題。
對(duì)于本地使用的數(shù)據(jù)結(jié)構(gòu),為提高內(nèi)存訪(fǎng)問(wèn)效率,采用四字節(jié)對(duì)齊方式;同時(shí)為了減少內(nèi)存的開(kāi)銷(xiāo),合理安排結(jié)構(gòu)體成員的位置,減少四字節(jié)對(duì)齊導(dǎo)致的成員之間的空隙,降低內(nèi)存開(kāi)銷(xiāo)。
對(duì)于處理器之間的數(shù)據(jù)結(jié)構(gòu),需要保證消息長(zhǎng)度不會(huì)因不同編譯平臺(tái)或處理器而導(dǎo)致消息結(jié)構(gòu)體長(zhǎng)度發(fā)生變化,使用一字節(jié)對(duì)齊方式對(duì)消息結(jié)構(gòu)進(jìn)行緊縮;為保證處理器之間的消息數(shù)據(jù)結(jié)構(gòu)的內(nèi)存訪(fǎng)問(wèn)效率,采用字節(jié)填充的方式自己對(duì)消息中成員進(jìn)行四字節(jié)對(duì)齊。
數(shù)據(jù)結(jié)構(gòu)的成員位置要兼顧成員之間的關(guān)系、數(shù)據(jù)訪(fǎng)問(wèn)效率和空間利用率。順序安排原則是:四字節(jié)的放在最前面,兩字節(jié)的緊接最后一個(gè)四字節(jié)成員,一字節(jié)緊接最后一個(gè)兩字節(jié)成員,填充字節(jié)放在最后。
舉例如下:

3.1.3.3 排查對(duì)齊問(wèn)題
如果出現(xiàn)對(duì)齊或者賦值問(wèn)題可查看:
編譯器的字節(jié)序大小端設(shè)置;
處理器架構(gòu)本身是否支持非對(duì)齊訪(fǎng)問(wèn);
如果支持看設(shè)置對(duì)齊與否,如果沒(méi)有則看訪(fǎng)問(wèn)時(shí)需要加某些特殊的修飾來(lái)標(biāo)志其特殊訪(fǎng)問(wèn)操作。
3.1.4 更改對(duì)齊方式
主要是更改C編譯器的缺省字節(jié)對(duì)齊方式。
在缺省情況下,C編譯器為每一個(gè)變量或是數(shù)據(jù)單元按其自然對(duì)界條件分配空間。一般地,可以通過(guò)下面的方法來(lái)改變?nèi)笔〉膶?duì)界條件:
使用偽指令#pragma pack(n):C編譯器將按照n個(gè)字節(jié)對(duì)齊;
使用偽指令#pragma pack(): 取消自定義字節(jié)對(duì)齊方式。
另外,還有如下的一種方式(GCC特有語(yǔ)法):
__attribute((aligned (n))): 讓所作用的結(jié)構(gòu)成員對(duì)齊在n字節(jié)自然邊界上。如果結(jié)構(gòu)體中有成員的長(zhǎng)度大于n,則按照最大成員的長(zhǎng)度來(lái)對(duì)齊。
__attribute__ ((packed)):取消結(jié)構(gòu)在編譯過(guò)程中的優(yōu)化對(duì)齊,按照實(shí)際占用字節(jié)數(shù)進(jìn)行對(duì)齊。
【注】__attribute__機(jī)制是GCC的一大特色,可以設(shè)置函數(shù)屬性(Function Attribute)、變量屬性(Variable Attribute)和類(lèi)型屬性(Type Attribute)。
下面具體針對(duì)MS VC/C++ 6.0編譯器介紹下如何修改編譯器默認(rèn)對(duì)齊值。
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)有/Zp[1|2|4|8|16],/Zpn表示以n字節(jié)邊界對(duì)齊。n字節(jié)邊界對(duì)齊是指一個(gè)成員的地址必須安排在成員的尺寸的整數(shù)倍地址上或者是n的整數(shù)倍地址上,取它們中的最小值。亦即:min(sizeof(member), n)。實(shí)際上,1字節(jié)邊界對(duì)齊也就表示結(jié)構(gòu)成員之間沒(méi)有空洞。
/Zpn選項(xiàng)應(yīng)用于整個(gè)工程,影響所有參與編譯的結(jié)構(gòu)體。在Struct member alignment中可選擇不同的對(duì)齊值來(lái)改變編譯選項(xiàng)。
在編碼時(shí),可用#pragma pack動(dòng)態(tài)修改對(duì)齊值。具體語(yǔ)法說(shuō)明見(jiàn)附錄5.3節(jié)。
自定義對(duì)齊值后要用#pragma pack()來(lái)還原,否則會(huì)對(duì)后面的結(jié)構(gòu)造成影響。
【例3】分析如下結(jié)構(gòu)體C:

變量b自身對(duì)齊值為1,指定對(duì)齊值為2,所以有效對(duì)齊值為1,假設(shè)C從0x0000開(kāi)始,則b存放在0x0000,符合0x0000%1= 0;變量a自身對(duì)齊值為4,指定對(duì)齊值為2,所以有效對(duì)齊值為2,順序存放在0x0002~0x0005四個(gè)連續(xù)字節(jié)中,符合0x0002%2=0。變量c的自身對(duì)齊值為2,所以有效對(duì)齊值為2,順序存放在0x0006~0x0007中,符合0x0006%2=0。所以從0x0000到0x00007共八字節(jié)存放的是C的變量。C的自身對(duì)齊值為4,所以其有效對(duì)齊值為2。又8%2=0,C只占用0x0000~0x0007的八個(gè)字節(jié)。所以sizeof(struct C) = 8。
注意,結(jié)構(gòu)體對(duì)齊到的字節(jié)數(shù)并非完全取決于當(dāng)前指定的pack值,如下:

另外,GNU GCC編譯器中按1字節(jié)對(duì)齊可寫(xiě)為以下形式:

此時(shí)sizeof(struct C)的值為7。
3.2 棧內(nèi)存對(duì)齊
在VC/C++中,棧的對(duì)齊方式不受結(jié)構(gòu)體成員對(duì)齊選項(xiàng)的影響??偸潜3謱?duì)齊且對(duì)齊在4字節(jié)邊界上。
【例4】

結(jié)果如下:

可以看出都是對(duì)齊到4字節(jié)。并且前面的char和short并沒(méi)有被湊在一起(成4字節(jié)),這和結(jié)構(gòu)體內(nèi)的處理是不同的。
至于為什么輸出的地址值是變小的,這是因?yàn)樵撈脚_(tái)下的棧是倒著“生長(zhǎng)”的。
3.3 位域?qū)R
3.3.1 位域定義
有些信息在存儲(chǔ)時(shí),并不需要占用一個(gè)完整的字節(jié),而只需占幾個(gè)或一個(gè)二進(jìn)制位。例如在存放一個(gè)開(kāi)關(guān)量時(shí),只有0和1兩種狀態(tài),用一位二進(jìn)位即可。為了節(jié)省存儲(chǔ)空間和處理簡(jiǎn)便,C語(yǔ)言提供了一種數(shù)據(jù)結(jié)構(gòu),稱(chēng)為“位域”或“位段”。
位域是一種特殊的結(jié)構(gòu)成員或聯(lián)合成員(即只能用在結(jié)構(gòu)或聯(lián)合中),用于指定該成員在內(nèi)存存儲(chǔ)時(shí)所占用的位數(shù),從而在機(jī)器內(nèi)更緊湊地表示數(shù)據(jù)。每個(gè)位域有一個(gè)域名,允許在程序中按域名操作對(duì)應(yīng)的位。這樣就可用一個(gè)字節(jié)的二進(jìn)制位域來(lái)表示幾個(gè)不同的對(duì)象。
位域定義與結(jié)構(gòu)定義類(lèi)似,其形式為:

其中位域列表的形式為:

位域的使用和結(jié)構(gòu)成員的使用相同,其一般形式為:

位域允許用各種格式輸出。
位域在本質(zhì)上就是一種結(jié)構(gòu)類(lèi)型,不過(guò)其成員是按二進(jìn)位分配的。位域變量的說(shuō)明與結(jié)構(gòu)變量說(shuō)明的方式相同,可先定義后說(shuō)明、同時(shí)定義說(shuō)明或直接說(shuō)明。
位域的使用主要為下面兩種情況:
①當(dāng)機(jī)器可用內(nèi)存空間較少而使用位域可大量節(jié)省內(nèi)存時(shí)。如把結(jié)構(gòu)作為大數(shù)組的元素時(shí)。
②當(dāng)需要把一結(jié)構(gòu)體或聯(lián)合映射成某預(yù)定的組織結(jié)構(gòu)時(shí)。如需要訪(fǎng)問(wèn)字節(jié)內(nèi)的特定位時(shí)。
3.3.2 對(duì)齊準(zhǔn)則
位域成員不能單獨(dú)被取sizeof值。下面主要討論含有位域的結(jié)構(gòu)體的sizeof。
C99規(guī)定int、unsigned int和bool可以作為位域類(lèi)型,但編譯器幾乎都對(duì)此作了擴(kuò)展,允許其它類(lèi)型的存在。位域作為嵌入式系統(tǒng)中非常常見(jiàn)的一種編程工具,優(yōu)點(diǎn)在于壓縮程序的存儲(chǔ)空間。
其對(duì)齊規(guī)則大致為:
(1)如果相鄰位域字段的類(lèi)型相同,且其位寬之和小于類(lèi)型的sizeof大小,則后面的字段將緊鄰前一個(gè)字段存儲(chǔ),直到不能容納為止;
(2)如果相鄰位域字段的類(lèi)型相同,但其位寬之和大于類(lèi)型的sizeof大小,則后面的字段將從新的存儲(chǔ)單元開(kāi)始,其偏移量為其類(lèi)型大小的整數(shù)倍;
(3)如果相鄰的位域字段的類(lèi)型不同,則各編譯器的具體實(shí)現(xiàn)有差異,VC6采取不壓縮方式,Dev-C++和GCC采取壓縮方式;
(4)如果位域字段之間穿插著非位域字段,則不進(jìn)行壓縮;
(5)整個(gè)結(jié)構(gòu)體的總大小為最寬基本類(lèi)型成員大小的整數(shù)倍,而位域則按照其最寬類(lèi)型字節(jié)數(shù)對(duì)齊。
【例5】

位域類(lèi)型為char,第1個(gè)字節(jié)僅能容納下element1和element2,所以element1和element2被壓縮到第1個(gè)字節(jié)中,而element3只能從下一個(gè)字節(jié)開(kāi)始。因此sizeof(BitField)的結(jié)果為2。
【例6】

由于相鄰位域類(lèi)型不同,在VC6中其sizeof為6,在Dev-C++中為2。
【例7】
非位域字段穿插在其中,不會(huì)產(chǎn)生壓縮,在VC6和Dev-C++中得到的大小均為3。
【例8】
位域中最寬類(lèi)型int的字節(jié)數(shù)為4,因此結(jié)構(gòu)體按4字節(jié)對(duì)齊,在VC6中其sizeof為16。
3.3.3 注意事項(xiàng)
關(guān)于位域操作有幾點(diǎn)需要注意:
(1)位域的地址不能訪(fǎng)問(wèn),因此不允許將&運(yùn)算符用于位域。不能使用指向位域的指針也不能使用位域的數(shù)組(數(shù)組是種特殊指針)。
例如,scanf函數(shù)無(wú)法直接向位域中存儲(chǔ)數(shù)據(jù):
intmain(void){structBitField1tBit;scanf("%d", &tBit.element2);//error: cannot take address of bit-field 'element2'return0;}
可用scanf函數(shù)將輸入讀入到一個(gè)普通的整型變量中,然后再賦值給tBit.element2。
(2)位域不能作為函數(shù)返回的結(jié)果。
(3)位域以定義的類(lèi)型為單位,且位域的長(zhǎng)度不能夠超過(guò)所定義類(lèi)型的長(zhǎng)度。例如定義int a:33是不允許的。
(4)位域可以不指定位域名,但不能訪(fǎng)問(wèn)無(wú)名的位域。
位域可以無(wú)位域名,只用作填充或調(diào)整位置,占位大小取決于該類(lèi)型。例如,char :0表示整個(gè)位域向后推一個(gè)字節(jié),即該無(wú)名位域后的下一個(gè)位域從下一個(gè)字節(jié)開(kāi)始存放,同理short :0和int :0分別表示整個(gè)位域向后推兩個(gè)和四個(gè)字節(jié)。
當(dāng)空位域的長(zhǎng)度為具體數(shù)值N時(shí)(如int :2),該變量?jī)H用來(lái)占位N位。
【例9】
結(jié)構(gòu)體大小為3。因?yàn)閑lement1占3位,后面要保留6位而char為8位,所以保留的6位只能放到第2個(gè)字節(jié)。同樣element3只能放到第3字節(jié)。
長(zhǎng)度為0的位域告訴編譯器將下一個(gè)位域放在一個(gè)存儲(chǔ)單元的起始位置。如上,編譯器會(huì)給成員element1分配3位,接著跳過(guò)余下的4位到下一個(gè)存儲(chǔ)單元,然后給成員element3分配5位。故上面的結(jié)構(gòu)體大小為2。
(5)位域的表示范圍。
位域的賦值不能超過(guò)其可以表示的范圍;
位域的類(lèi)型決定該編碼能表示的值的結(jié)果。
對(duì)于第二點(diǎn),若位域?yàn)閡nsigned類(lèi)型,則直接轉(zhuǎn)化為正數(shù);若非unsigned類(lèi)型,則先判斷最高位是否為1,若為1表示補(bǔ)碼,則對(duì)其除符號(hào)位外的所有位取反再加一得到最后的結(jié)果數(shù)據(jù)(原碼)。如:
(6)帶位域的結(jié)構(gòu)在內(nèi)存中各個(gè)位域的存儲(chǔ)方式取決于編譯器,既可從左到右也可從右到左存儲(chǔ)。
【例10】在VC6下執(zhí)行下面的代碼:
輸入i值為11,則輸出為i = 11, cba = -2 -1 -1。
Intel x86處理器按小字節(jié)序存儲(chǔ)數(shù)據(jù),所以bits中的位域在內(nèi)存中放置順序?yàn)閏cba。當(dāng)num.i置為11時(shí),bits的最低有效位(即位域a)的值為1,a、b、c按低地址到高地址分別存儲(chǔ)為10、1、1(二進(jìn)制)。
但為什么最后的打印結(jié)果是a=-1而不是1?
因?yàn)槲挥騛定義的類(lèi)型signed char是有符號(hào)數(shù),所以盡管a只有1位,仍要進(jìn)行符號(hào)擴(kuò)展。1做為補(bǔ)碼存在,對(duì)應(yīng)原碼-1。
如果將a、b、c的類(lèi)型定義為unsigned char,即可得到cba = 2 1 1。1011即為11的二進(jìn)制數(shù)。
注:C語(yǔ)言中,不同的成員使用共同的存儲(chǔ)區(qū)域的數(shù)據(jù)構(gòu)造類(lèi)型稱(chēng)為聯(lián)合(或共用體)。聯(lián)合占用空間的大小取決于類(lèi)型長(zhǎng)度最大的成員。聯(lián)合在定義、說(shuō)明和使用形式上與結(jié)構(gòu)體相似。
(7)位域的實(shí)現(xiàn)會(huì)因編譯器的不同而不同,使用位域會(huì)影響程序可移植性。因此除非必要否則最好不要使用位域。
(8)盡管使用位域可以節(jié)省內(nèi)存空間,但卻增加了處理時(shí)間。當(dāng)訪(fǎng)問(wèn)各個(gè)位域成員時(shí),需要把位域從它所在的字中分解出來(lái)或反過(guò)來(lái)把一值壓縮存到位域所在的字位中。
四,總結(jié)
讓我們回到引言部分的問(wèn)題。
缺省情況下,C/C++編譯器默認(rèn)將結(jié)構(gòu)、棧中的成員數(shù)據(jù)進(jìn)行內(nèi)存對(duì)齊。因此,引言程序輸出就變成c1 -> 0, s -> 2, c2 -> 4, i -> 8。
編譯器將未對(duì)齊的成員向后移,將每一個(gè)都成員對(duì)齊到自然邊界上,從而也導(dǎo)致整個(gè)結(jié)構(gòu)的尺寸變大。盡管會(huì)犧牲一點(diǎn)空間(成員之間有空洞),但提高了性能。
也正是這個(gè)原因,引言例子中sizeof(T_ FOO)為12,而不是8。
總結(jié)說(shuō)來(lái),就是:
(1)在結(jié)構(gòu)體中,綜合考慮變量本身和指定的對(duì)齊值;
(2)在棧上,不考慮變量本身的大小,統(tǒng)一對(duì)齊到4字節(jié)。

學(xué)習(xí)C/C++編程知識(shí),提升C/C++編程能力,歡迎關(guān)注UP一起來(lái)成長(zhǎng)!
另外,UP在主頁(yè)上傳了一些學(xué)習(xí)C/C++編程的視頻教程,有興趣或者正在學(xué)習(xí)的小伙伴一定要去看一看哦!會(huì)對(duì)你有幫助的~