位欄位(bit-field)是一個由具有特定數量的位組成的整數變數。結構或聯合的成員也可以是位欄位。如果連續宣告多個小的位欄位,編譯器會將它們合併成一個機器字(word)。這使得小單元資訊具有更加緊湊的儲存方式。當然,也可以使用位運算子來獨立處理特定位,但是位欄位允許我們利用名稱來處理位,類似於結構或聯合的成員。
位欄位的宣告格式為:
型別[成員名稱]:寬度;
各部分的詳細描述如下:
(1) 型別
指定一個整數型別,用來決定該位欄位值被解釋的方式。型別可以是 _Bool、int、signed int、unsigned int,或者為所選實現版本所提供的型別。這裡的型別也可以包含型別限定符。
具有 signed int 型別的位欄位會被解釋成有符號數;具有 unsigned int 型別的位欄位會被解釋成無符號數。具有 int 型別的位欄位可以是有符號或無符號的型別,由編譯器決定。
(2) 成員名稱
成員名稱是可選的(可以不寫)。但是,如果宣告了一個無名稱的位欄位,就沒有辦法獲取它。沒有名稱的位欄位只能用於填充(padding),以幫助後續的位欄位在機器字中對齊到特定的地址邊界。
(3) 寬度
位欄位中位的數量。
寬度必須是一個常數整數表示式,其值是非負的,並且必須小於或等於指定型別的位寬。無名稱位欄位的寬度可以是 0。在這種情況下,下一個宣告的位欄位就會從新的可定址記憶體單元開始。
當在一個結構或聯合內宣告一個位欄位的時候,編譯器會分配一個足以容納它的可定址記憶體單元。通常情況下,被分配的記憶體單元是一個 int 型別的機器字。
如果緊接著的位欄位適合同一記憶體單元中剩下的空間,那麼就被定義到與前面的位欄位緊鄰的位置。如果不適合的話,那麼編譯器就分配另外的記憶體單元,並在新單元的起始放置下一個位欄位,或者跨過前一個記憶體單元的結尾和下一個記憶體單元的開頭。
下面的例子重新定義了結構型別 struct Date,讓其成員 month 和 day 只佔據各自需要的位數。為了展示 _Bool 型別的位欄位,我們為夏令時設定一個標籤。這段程式碼以目標機器使用至少 32 位字為前提:
struct Date {
unsigned int month : 4; // 1是1月;12是12月
unsigned int day : 5; // 月份中的日(1~31)
signed int year : 22; // (-2097152~+2097151)
_Bool isDST : 1; // 如果是夏令時,則為true
};
n 位的位欄位可以有 2n 個不同的值。結構成員 month 的取值範圍是 0~15;成員 day 的取值範圍是 0~31;成員 year 的值範圍是 -2097152~+2097151。我們可以使用常見的初始化列表方式初始化一個 struct Date 型別的物件:
struct Date birthday = { 5, 17, 1982 };
物件 birthday 占據的儲存空間大小與一個 32 位的 int 整數物件一樣。
和結構中其他成員所不同的是,位欄位通常不會佔據可定址的記憶體位置,因此無法對位欄位採用地址運算子(&)或宏 offsetof。
但是在其他方面,可以將位欄位看作結構或聯合成員,使用點和箭頭運算子來獲取,並以類似對待 int 或 unsigned int 變數的方式對其進行算術運算。因此,使用位欄位重新定義的 Date 結構在函數 dateAsString()中不需作任何修改:
const char *dateAsString( struct Date d )
{
static char strDate[12];
sprintf( strDate, "%02d/%02d/%04d", d.month, d.day, d.year );
return strDate;
}
下面的語句為物件 birthday 呼叫函數 dateAsString(),並採用標準函數 puts()輸出結果:
puts( dateAsString( birthday ));