讀古今文學網 > CPrimerPlus(第6版)(中文版) > 第15章 位操作 >

第15章 位操作

本章介紹以下內容:

運算符:~、&、|、^、

<<、>>

&=、|=、^=、>>=、<<=

二進制、十進制和十六進制記數法(複習)

處理一個值中的位的兩個C工具:位運算符和位字段

關鍵字:_Alignas、_Alignof

在C語言中,可以單獨操控變量中的位。讀者可能好奇,竟然有人想這樣做。有時必須單獨操控位,而且非常有用。例如,通常向硬件設備發送一兩個字節來控制這些設備,其中每個位(bit)都有特定的含義。另外,與文件相關的操作系統信息經常被儲存,通過使用特定位表明特定項。許多壓縮和加密操作都是直接處理單獨的位。高級語言一般不會處理這級別的細節,C 在提供高級語言便利的同時,還能在為彙編語言所保留的級別上工作,這使其成為編寫設備驅動程序和嵌入式代碼的首選語言。

首先要介紹位、字節、二進制記數法和其他進制記數系統的一些背景知識。

15.1 二進制數、位和字節

通常都是基於數字10來書寫數字。例如2157的千位是2,百位是1,十位是5,個位是7,可以寫成:

2×1000 + 1×100 + 5×10 + 7×1

注意,1000是10的立方(即3次冪),100是10的平方(即2次冪),10是10的1次冪,而且10(以及任意正數)的0次冪是1。因此,2157也可以寫成:

2×103+ 1×102+ 5×101+ 7×100

因為這種書寫數字的方法是基於10的冪,所以稱以10為基底書寫2157。

姑且認為十進制系統得以發展是得益於我們都有10根手指。從某種意義上看,計算機的位只有2根手指,因為它只能被設置為0或1,關閉或打開。因此,計算機適用基底為2的數制系統。它用2的冪而不是10的冪。以2為基底表示的數字被稱為二進制數(binary number)。二進制中的2和十進制中的10作用相同。例如,二進制數1101可表示為:

1×23+ 1×22+ 0×21+ 1×20

以十進制數表示為:

1×8 + 1×4 + 0×2 + 1×1 = 13

用二進制系統可以把任意整數(如果有足夠的位)表示為0和1的組合。由於數字計算機通過關閉和打開狀態的組合來表示信息,這兩種狀態分別用0和1來表示,所以使用這套數制系統非常方便。接下來,我們來學習二進制系統如何表示1字節的整數。

15.1.1 二進制整數

通常,1字節包含8位。C語言用字節(byte)表示儲存系統字符集所需的大小,所以C字節可能是8位、9位、16位或其他值。不過,描述存儲器芯片和數據傳輸率中所用的字節指的是8位字節。為了簡化起見,本章假設1字節是8位(計算機界通常用八位組(octet)這個術語特指8位字節)。可以從左往右給這8位分別編號為7~0。在1字節中,編號是7的位被稱為高階位(high-order bit),編號是0的位被稱為低階位(low-order bit)。每 1位的編號對應2的相應指數。因此,可以根據圖15.1所示的例子理解字節。

圖15.1 位編號和位值

這裡,128是2的7次冪,以此類推。該字節能表示的最大數字是把所有位都設置為1:11111111。這個二進制數的值是:

128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255

而該字節最小的二進制數是00000000,其值為0。因此,1字節可儲存0~255範圍內的數字,總共256個值。或者,通過不同的方式解釋位組合(bit pattern),程序可以用1字節儲存-128~+127範圍內的整數,總共還是256個值。例如,通常unsigned char用1字節表示的範圍是0~255,而signed char用1字節表示的範圍是-128~+127。

15.1.2 有符號整數

如何表示有符號整數取決於硬件,而不是C語言。也許表示有符號數最簡單的方式是用1位(如,高階位)儲存符號,只剩下7位表示數字本身(假設儲存在1字節中)。用這種符號量(sign-magnitude)表示法,10000001表示−1,00000001表示1。因此,其表示範圍是−127~+127。

這種方法的缺點是有兩個0:+0和-0。這很容易混淆,而且用兩個位組合來表示一個值也有些浪費。

二進制補碼(two』s-complement)方法避免了這個問題,是當今最常用的系統。我們將以1字節為例,討論這種方法。二進制補碼用1字節中的後7位表示0~127,高階位設置為0。目前,這種方法和符號量的方法相同。另外,如果高階位是1,表示的值為負。這兩種方法的區別在於如何確定負值。從一個9位組合100000000(256的二進制形式)減去一個負數的位組合,結果是該負值的量。例如,假設一個負值的位組合是 10000000,作為一個無符號字節,該組合為表示 128;作為一個有符號值,該組合表示負值(編碼是 7的位為1),而且值為100000000-10000000,即 1000000(128)。因此,該數是-128(在符號量表示法中,該位組合表示−0)。類似地,10000001 是−127,11111111 是−1。該方法可以表示−128~+127範圍內的數。

要得到一個二進制補碼數的相反數,最簡單的方法是反轉每一位(即0變為1,1變為0),然後加1。因為1是00000001,那麼−1則是11111110+1,或11111111。這與上面的介紹一致。

二進制反碼(one』s-complement)方法通過反轉位組合中的每一位形成一個負數。例如,00000001是1,那麼11111110是−1。這種方法也有一個−0:11111111。該方法能表示-127~+127之間的數。

15.1.3 二進制浮點數

浮點數分兩部分儲存:二進制小數和二進制指數。下面我們將詳細介紹。

1.二進制小數

一個普通的浮點數0.527,表示如下:

5/10 + 2/100 + 7/1000

從左往右,各分母都是10的遞增次冪。在二進制小數中,使用2的冪作為分母,所以二進制小數.101表示為:

1/2 + 0/4 + 1/8

用十進製表示法為:

0.50 + 0.00 + 0.125

即是0.625。

許多分數(如,1/3)不能用十進製表示法精確地表示。與此類似,許多分數也不能用二進製表示法準確地表示。實際上,二進製表示法只能精確地表示多個1/2的冪的和。因此,3/4和7/8可以精確地表示為二進制小數,但是1/3和2/5卻不能。

2.浮點數表示法

為了在計算機中表示一個浮點數,要留出若干位(因系統而異)儲存二進制分數,其他位儲存指數。一般而言,數字的實際值是由二進制小數乘以2的指定次冪組成。例如,一個浮點數乘以4,那麼二進制小數不變,其指數乘以2,二進制分數不變。如果一份浮點數乘以一個不是2的冪的數,會改變二進制小數部分,如有必要,也會改變指數部分。

15.2 其他進制數

計算機界通常使用八進制記數系統和十六進制記數系統。因為8和16都是2的冪,這些系統比十進制系統更接近計算機的二進制系統。

15.2.1 八進制

八進制(octal)是指八進制記數系統。該系統基於8的冪,用0~7表示數字(正如十進制用0~9表示數字一樣)。例如,八進制數451(在C中寫作0451)表示為:

4×82+ 5×81+ 1×80= 297(十進制)

瞭解八進制的一個簡單的方法是,每個八進制位對應3個二進制位。表15.1列出了這種對應關係。這種關係使得八進制與二進制之間的轉換很容易。例如,八進制數0377的二進制形式是11111111。即,用111代替0377中的最後一個7,再用111代替倒數第2個7,最後用011代替3,並捨去第1位的0。這表明比0377大的八進制要用多個字節表示。這是八進制唯一不方便的地方:一個3位的八進制數可能要用9位二進制數來表示。注意,將八進制數轉換為二進制形式時,不能去掉中間的0。例如,八進制數0173的二進制形式是01111011,不是0111111。

表15.1 與八進制位等價的二進制位

15.2.2 十六進制

十六進制(hexadecimal或hex)是指十六進制記數系統。該系統基於16的冪,用0~15表示數字。但是,由於沒有單獨的數(digit,即0~9這樣單獨一位的數)表示10~15,所以用字母A~F來表示。例如,十六進制數A3F(在C中寫作0xA3F)表示為:

10×162+3×161+ 15×160= 2623(十進制)

由於A表示10,F表示15。在C語言中,A~F既可用小寫也可用大寫。因此,2623也可寫作0xa3f。

每個十六進制位都對應一個4位的二進制數(即4個二進制位),那麼兩個十六進制位恰好對應一個8位字節。第1個十六進製表示前4位,第2個十六進制位表示後4位。因此,十六進制很適合表示字節值。

表15.2列出了各進制之間的對應關係。例如,十六進制值0xC2可轉換為11000010。相反,二進制值11010101可以看作是1101 0101,可轉換為0xD5。

表15.2 十進制、十六進制和等價的二進制

介紹了位和字節的相關內容,接下來我們研究C用位和字節進行哪些操作。C有兩個操控位的工具。第 1 個工具是一套(6 個)作用於位的按位運算符。第 2 個工具是字段(field)數據形式,用於訪問 int中的位。下面將簡要介紹這些C的特性。

15.3 C按位運算符

C 提供按位邏輯運算符和移位運算符。在下面的例子中,為了方便讀者瞭解位的操作,我們用二進制記數法寫出值。但是在實際的程序中不必這樣,用一般形式的整型變量或常量即可。例如,在程序中用25或031或0x19,而不是00011001。另外,下面的例子均使用8位二進制數,從左往右每位的編號為7~0。

15.3.1 按位邏輯運算符

4個按位邏輯運算符都用於整型數據,包括char。之所以叫作按位(bitwise)運算,是因為這些操作都是針對每一個位進行,不影響它左右兩邊的位。不要把這些運算符與常規的邏輯運算符(&&、||和!)混淆,常規的邏輯運算符操作的是整個值。

1.二進制反碼或按位取反:~

一元運算符~把1變為0,把0變為1。如下例子所示:

~(10011010) // 表達式

(01100101)// 結果值

假設val的類型是unsigned char,已被賦值為2。在二進制中,00000010表示2。那麼,~val的值是11111101,即253。注意,該運算符不會改變val的值,就像3 * val不會改變val的值一樣, val仍然是2。但是,該運算符確實創建了一個可以使用或賦值的新值:

newval = ~val;

printf("%d", ~val);

如果要把val的值改為~val,使用下面這條語句:

val = ~val;

2.按位與:&

二元運算符&通過逐位比較兩個運算對象,生成一個新值。對於每個位,只有兩個運算對像中相應的位都為1時,結果才為1(從真/假方面看,只有當兩個位都為真時,結果才為真)。因此,對下面的表達式求值:

(10010011) & (00111101)// 表達式

由於兩個運算對像中編號為4和0的位都為1,得:

(00010001)// 結果值

C有一個按位與和賦值結合的運算符:&=。下面兩條語句產生的最終結果相同:

val &= 0377;

val = val & 0377;

3.按位或:|

二元運算符|,通過逐位比較兩個運算對象,生成一個新值。對於每個位,如果兩個運算對像中相應的位為1,結果就為1(從真/假方面看,如果兩個運算對像中相應的一個位為真或兩個位都為真,那麼結果為真)。因此,對下面的表達式求值:

(10010011) | (00111101) // 表達式

除了編號為6的位,這兩個運算對象的其他位至少有一個位為1,得:

(10111111) // 結果值

C有一個按位或和賦值結合的運算符:|=。下面兩條語句產生的最終作用相同:

val |= 0377;

val = val | 0377;

4.按位異或:^

二元運算符^逐位比較兩個運算對象。對於每個位,如果兩個運算對像中相應的位一個為1(但不是兩個為1),結果為1(從真/假方面看,如果兩個運算對像中相應的一個位為真且不是兩個為同為1,那麼結果為真)。因此,對下面表達式求值:

(10010011) ^ (00111101) // 表達式

編號為0的位都是1,所以結果為0,得:

(10101110)// 結果值

C有一個按位異或和賦值結合的運算符:^=。下面兩條語句產生的最終作用相同:

val ^= 0377;

val = val ^ 0377;

15.3.2 用法:掩碼

按位與運算符常用於掩碼(mask)。所謂掩碼指的是一些設置為開(1)或關(0)的位組合。要明白稱其為掩碼的原因,先來看通過&把一個量與掩碼結合後發生什麼情況。例如,假設定義符號常量MASK為2 (即,二進制形式為00000010),只有1號位是1,其他位都是0。下面的語句:

flags = flags & MASK;

把flags中除1號位以外的所有位都設置為0,因為使用按位與運算符(&)任何位與0組合都得0。1號位的值不變(如果1號位是1,那麼 1&1得1;如果 1號位是0,那麼 0&1也得0)。這個過程叫作「使用掩碼」,因為掩碼中的0隱藏了flags中相應的位。

可以這樣類比:把掩碼中的0看作不透明,1看作透明。表達式flags & MASK相當於用掩碼覆蓋在flags的位組合上,只有MASK為1的位才可見(見圖15.2)。

圖15.2 掩碼示例

用&=運算符可以簡化前面的代碼,如下所示:

flags &= MASK;

下面這條語句是按位與的一種常見用法:

ch &= 0xff; /* 或者 ch &= 0377; */

前面介紹過oxff的二進制形式是11111111,八進制形式是0377。這個掩碼保持ch中最後8位不變,其他位都設置為0。無論ch原來是8位、16位或是其他更多位,最終的值都被修改為1個8位字節。在該例中,掩碼的寬度為8位。

15.3.3 用法:打開位(設置位)

有時,需要打開一個值中的特定位,同時保持其他位不變。例如,一台 IBM PC 通過向端口發送值來控制硬件。例如,為了打開內置揚聲器,必須打開 1 號位,同時保持其他位不變。這種情況可以使用按位或運算符(|)。

以上一節的flags和MASK(只有1號位為1)為例。下面的語句:

flags = flags | MASK;

把flags的1號位設置為1,且其他位不變。因為使用|運算符,任何位與0組合,結果都為本身;任何位與1組合,結果都為1。

例如,假設flags是00001111,MASK是10110110。下面的表達式:

flags | MASK

即是:

(00001111) | (10110110)// 表達式

其結果為:

(10111111) // 結果值

MASK中為1的位,flags與其對應的位也為1。MASK中為0的位,flags與其對應的位不變。

用|=運算符可以簡化上面的代碼,如下所示:

flags |= MASK;

同樣,這種方法根據MASK中為1的位,把flags中對應的位設置為1,其他位不變。

15.3.4 用法:關閉位(清空位)

和打開特定的位類似,有時也需要在不影響其他位的情況下關閉指定的位。假設要關閉變量flags中的1號位。同樣,MASK只有1號位為1(即,打開)。可以這樣做:

flags = flags & ~MASK;

由於MASK除1號位為1以外,其他位全為0,所以~MASK除1號位為0以外,其他位全為1。使用&,任何位與1組合都得本身,所以這條語句保持1號位不變,改變其他各位。另外,使用&,任何位與0組合都的0。所以無論1號位的初始值是什麼,都將其設置為0。

例如,假設flags是00001111,MASK是10110110。下面的表達式:

flags & ~MASK

即是:

(00001111) & ~(10110110) // 表達式

其結果為:

(00001001) // 結果值

MASK中為1的位在結果中都被設置(清空)為0。flags中與MASK為0的位相應的位在結果中都未改變。

可以使用下面的簡化形式:

flags &= ~MASK;

15.3.5 用法:切換位

切換位指的是打開已關閉的位,或關閉已打開的位。可以使用按位異或運算符(^)切換位。也就是說,假設b是一個位(1或0),如果b為1,則1^b為0;如果b為0,則1^b為1。另外,無論b為1還是0,0^b均為b。因此,如果使用^組合一個值和一個掩碼,將切換該值與MASK為1的位相對應的位,該

值與MASK為0的位相對應的位不變。要切換flags中的1號位,可以使用下面兩種方法:

flags = flags ^ MASK;

flags ^= MASK;

例如,假設flags是00001111,MASK是10110110。表達式:

flags ^ MASK

即是:

(00001111) ^ (10110110)// 表達式

其結果為:

(10111001) // 結果值

flags中與MASK為1的位相對應的位都被切換了,MASK為0的位相對應的位不變。

15.3.6 用法:檢查位的值

前面介紹了如何改變位的值。有時,需要檢查某位的值。例如,flags中1號位是否被設置為1?不能這樣直接比較flags和MASK:

if (flags == MASK)

puts("Wow!"); /* 不能正常工作 */

這樣做即使flags的1號位為1,其他位的值會導致比較結果為假。因此,必須覆蓋flags中的其他位,只用1號位和MASK比較:

if ((flags & MASK) == MASK)

puts("Wow!");

由於按位運算符的優先級比==低,所以必須在flags & MASK周圍加上圓括號。

為了避免信息漏過邊界,掩碼至少要與其覆蓋的值寬度相同。

15.3.7 移位運算符

下面介紹C的移位運算符。移位運算符向左或向右移動位。同樣,我們在示例中仍然使用二進制數,有助於讀者理解其工作原理。

1.左移:<<

左移運算符(<<)將其左側運算對像每一位的值向左移動其右側運算對像指定的位數。左側運算對像移出左末端位的值丟失,用0填充空出的位置。下面的例子中,每一位都向左移動兩個位置:

(10001010) << 2  // 表達式

(00101000)// 結果值

該操作產生了一個新的位值,但是不改變其運算對象。例如,假設stonk為1,那麼 stonk<<2為4,但是stonk本身不變,仍為1。可以使用左移賦值運算符(<<=)來更改變量的值。該運算符將變量中的位向左移動其右側運算對像給定值的位數。如下例:

int stonk = 1;

int onkoo;

onkoo = stonk << 2;  /* 把4賦給onkoo */

stonk <<= 2; /* 把stonk的值改為4 */

2.右移:>>

右移運算符(>>)將其左側運算對像每一位的值向右移動其右側運算對像指定的位數。左側運算對像移出右末端位的值丟。對於無符號類型,用 0 填充空出的位置;對於有符號類型,其結果取決於機器。空出的位置可用0填充,或者用符號位(即,最左端的位)的副本填充:

(10001010) >> 2// 表達式,有符號值

(00100010) // 在某些系統中的結果值

(10001010) >> 2// 表達式,有符號值

(11100010) // 在另一些系統上的結果值

下面是無符號值的例子:

(10001010) >> 2// 表達式,無符號值

(00100010) // 所有系統都得到該結果值

每個位向右移動兩個位置,空出的位用0填充。

右移賦值運算符(>>=)將其左側的變量向右移動指定數量的位數。如下所示:

int sweet = 16;

int ooosw;

ooosw = sweet >> 3;// ooosw = 2,sweet的值仍然為16

sweet >>=3;// sweet的值為2

3.用法:移位運算符

移位運算符針對2的冪提供快速有效的乘法和除法:

number << n  number乘以2的n次冪

number >> n  如果number為非負,則用number除以2的n次冪

這些移位運算符類似於在十進制中移動小數點來乘以或除以10。

移位運算符還可用於從較大單元中提取一些位。例如,假設用一個unsigned long類型的值表示顏色值,低階位字節儲存紅色的強度,下一個字節儲存綠色的強度,第 3 個字節儲存藍色的強度。隨後你希望把每種顏色的強度分別儲存在3個不同的unsigned char類型的變量中。那麼,可以使用下面的語句:

#define BYTE_MASK 0xff

unsigned long color = 0x002a162f;

unsigned char blue, green, red;

red = color & BYTE_MASK;

green = (color >> 8) & BYTE_MASK;

blue = (color >> 16) & BYTE_MASK;

以上代碼中,使用右移運算符將 8 位顏色值移動至低階字節,然後使用掩碼技術把低階字節賦給指定的變量。

15.3.8 編程示例

在第 9 章中,我們用遞歸的方法編寫了一個程序,把數字轉換為二進制形式(程序清單 9.8)。現在,要用移位運算符來解決相同的問題。程序清單15.1中的程序,讀取用戶從鍵盤輸入的整數,將該整數和一個字符串地址傳遞給itobs函數(itobs表示interger to binary string,即整數轉換成二進制字符串)。然後,該函數使用移位運算符計算出正確的1和0的組合,並將其放入字符串中。

程序清單15.1 binbit.c程序

/* binbit.c -- 使用位操作顯示二進制 */

#include <stdio.h>

#include <limits.h> // 提供 CHAR_BIT 的定義,CHAR_BIT 表示每字節的位數

char * itobs(int, char *);

void show_bstr(const char *);

int main(void)

{

char bin_str[CHAR_BIT * sizeof(int) + 1];

int number;

puts("Enter integers and see them in binary.");

puts("Non-numeric input terminates program.");

while (scanf("%d", &number) == 1)

{

itobs(number, bin_str);

printf("%d is ", number);

show_bstr(bin_str);

putchar('\n');

}

puts("Bye!");

return 0;

}

char * itobs(int n, char * ps)

{

int i;

const static int size = CHAR_BIT * sizeof(int);

for (i = size - 1; i >= 0; i--, n >>= 1)

ps[i] = (01 & n) + '0';

ps[size] = '\0';

return ps;

}

/*4位一組顯示二進制字符串 */

void show_bstr(const char * str)

{

int i = 0;

while (str[i]) /* 不是一個空字符 */

{

putchar(str[i]);

if (++i % 4 == 0 && str[i])

putchar(' ');

}

}

程序清單15.1使用limits.h中的CHAR_BIT宏,該宏表示char中的位數。sizeof運算符返回char的大小,所以表達式CHAE_BIT * sizeof(int)表示int類型的位數。bin_str數組的元素個數是CHAE_BIT * sizeof(int) + 1,留出一個位置給末尾的空字符。

itobs函數返回的地址與傳入的地址相同,可以把該函數作為printf的參數。在該函數中,首次執行for循環時,對01 & n求值。01是一個八進制形式的掩碼,該掩碼除0號位是1之外,其他所有位都為0。因此,01 & n就是n最後一位的值。該值為0或1。但是對數組而言,需要的是字符'0'或字符'1'。該值加上'0'即可完成這種轉換(假設按順序編碼的數字,如 ASCII)。其結果存放在數組中倒數第2個元素中(最後一個元素用來存放空字符)。

順帶一提,用1 & n或01 & n都可以。我們用八進制1而不是十進制1,只是為了更接近計算機的表達方式。

然後,循環執行i--和n >>= 1。i--移動到數組的前一個元素,n >>= 1使n中的所有位向右移動一個位置。進入下一輪迭代時,循環中處理的是n中新的最右端的值。然後,把該值儲存在倒數第3個元素中,以此類推。itobs函數用這種方式從右往左填充數組。

可以使用printf或puts函數顯示最終的字符串,但是程序清單15.1中定義了show_bstr函數,以4位一組打印字符串,方便閱讀。

下面的該程序的運行示例:

Enter integers and see them in binary.

Non-numeric input terminates program.

7

7 is 0000 0000 0000 0000 0000 0000 0000 0111

2013

2013 is 0000 0000 0000 0000 0000 0111 1101 1101

-1

-1 is 1111 1111 1111 1111 1111 1111 1111 1111

32123

32123 is 0000 0000 0000 0000 0111 1101 0111 1011

q

Bye!

15.3.9 另一個例子

我們來看另一個例子。這次要編寫的函數用於切換一個值中的後 n 位,待處理值和 n 都是函數的參數。

~運算符切換一個字節的所有位,而不是選定的少數位。但是,^運算符(按位異或)可用於切換單個位。假設創建了一個掩碼,把後n位設置為1,其餘位設置為0。然後使用^組合掩碼和待切換的值便可切換該值的最後n位,而且其他位不變。方法如下:

int invert_end(int num, int bits)

{

int mask = 0;

int bitval = 1;

while (bits–– > 0)

{

mask |= bitval;

bitval <<= 1;

}

return num ^ mask;

}

while循環用於創建所需的掩碼。最初,mask的所有位都為0。第1輪循環將mask的0號位設置為1。然後第2輪循環將mask的1號位設置為1,以此類推。循環bits次,mask的後bits位就都被設置為1。最後,num ^ mask運算即得所需的結果。

我們把這個函數放入前面的程序中,測試該函數。如程序清單15.2所示。

程序清單15.2 invert4.c程序

/* invert4.c -- 使用位操作顯示二進制 */

#include <stdio.h>

#include <limits.h>

char * itobs(int, char *);

void show_bstr(const char *);

int invert_end(int num, int bits);

int main(void)

{

char bin_str[CHAR_BIT * sizeof(int) + 1];

int number;

puts("Enter integers and see them in binary.");

puts("Non-numeric input terminates program.");

while (scanf("%d", &number) == 1)

{

itobs(number, bin_str);

printf("%d is\n", number);

show_bstr(bin_str);

putchar('\n');

number = invert_end(number, 4);

printf("Inverting the last 4 bits gives\n");

show_bstr(itobs(number, bin_str));

putchar('\n');

}

puts("Bye!");

return 0;

}

char * itobs(int n, char * ps)

{

int i;

const static int size = CHAR_BIT * sizeof(int);

for (i = size - 1; i >= 0; i--, n >>= 1)

ps[i] = (01 & n) + '0';

ps[size] = '\0';

return ps;

}

/* 以4位為一組,顯示二進制字符串 */

void show_bstr(const char * str)

{

int i = 0;

while (str[i]) /* 不是空字符 */

{

putchar(str[i]);

if (++i % 4 == 0 && str[i])

putchar(' ');

}

}

int invert_end(int num, int bits)

{

int mask = 0;

int bitval = 1;

while (bits-- > 0)

{

mask |= bitval;

bitval <<= 1;

}

return num ^ mask;

}

下面是該程序的一個運行示例:

Enter integers and see them in binary.

Non-numeric input terminates program.

7

7 is

0000 0000 0000 0000 0000 0000 0000 0111

Inverting the last 4 bits gives

0000 0000 0000 0000 0000 0000 0000 1000

12541

12541 is

0000 0000 0000 0000 0011 0000 1111 1101

Inverting the last 4 bits gives

0000 0000 0000 0000 0011 0000 1111 0010

q

Bye!

15.4 位字段

操控位的第2種方法是位字段(bit field)。位字段是一個signed int或unsigned int類型變量中的一組相鄰的位(C99和C11新增了_Bool類型的位字段)。位字段通過一個結構聲明來建立,該結構聲明為每個字段提供標籤,並確定該字段的寬度。例如,下面的聲明建立了一個4個1位的字段:

struct {

unsigned int autfd : 1;

unsigned int bldfc : 1;

unsigned int undln : 1;

unsigned int itals : 1;

} prnt;

根據該聲明,prnt包含4個1位的字段。現在,可以通過普通的結構成員運算符(.)單獨給這些字段賦值:

prnt.itals = 0;

prnt.undln = 1;

由於每個字段恰好為1位,所以只能為其賦值1或0。變量prnt被儲存在int大小的內存單元中,但是在本例中只使用了其中的4位。

帶有位字段的結構提供一種記錄設置的方便途徑。許多設置(如,字體的粗體或斜體)就是簡單的二選一。例如,開或關、真或假。如果只需要使用 1 位,就不需要使用整個變量。內含位字段的結構允許在一個存儲單元中儲存多個設置。

有時,某些設置也有多個選擇,因此需要多位來表示。這沒問題,字段不限制 1 位大小。可以使用如下的代碼:

struct {

unsigned int code1 : 2;

unsigned int code2 : 2;

unsigned int code3 : 8;

} prcode;

以上代碼創建了兩個2位的字段和一個8位的字段。可以這樣賦值:

prcode.code1 = 0;

prcode.code2 = 3;

prcode.code3 = 102;

但是,要確保所賦的值不超出字段可容納的範圍。

如果聲明的總位數超過了一個unsigned int類型的大小會怎樣?會用到下一個unsigned int類型的存儲位置。一個字段不允許跨越兩個unsigned int之間的邊界。編譯器會自動移動跨界的字段,保持unsigned int的邊界對齊。一旦發生這種情況,第1個unsigned int中會留下一個未命名的「洞」。

可以用未命名的字段寬度「填充」未命名的「洞」。使用一個寬度為0的未命名字段迫使下一個字段與下一個整數對齊:

struct {

unsigned int field1: 1 ;

unsigned int  : 2 ;

unsigned int field2: 1 ;

unsigned int  : 0 ;

unsigned int field3: 1 ;

} stuff;

這裡,在stuff.field1和stuff.field2之間,有一個2位的空隙;stuff.field3將儲存在下一個unsigned int中。

字段儲存在一個int中的順序取決於機器。在有些機器上,存儲的順序是從左往右,而在另一些機器上,是從右往左。另外,不同的機器中兩個字段邊界的位置也有區別。由於這些原因,位字段通常都不容易移植。儘管如此,有些情況卻要用到這種不可移植的特性。例如,以特定硬件設備所用的形式儲存數據。

15.4.1 位字段示例

通常,把位字段作為一種更緊湊儲存數據的方式。例如,假設要在屏幕上表示一個方框的屬性。為簡化問題,我們假設方框具有如下屬性:

方框是透明的或不透明的;

方框的填充色選自以下調色板:黑色、紅色、綠色、黃色、藍色、紫色、青色或白色;

邊框可見或隱藏;

邊框顏色與填充色使用相同的調色板;

邊框可以使用實線、點線或虛線樣式。

可以使用單獨的變量或全長(full-sized)結構成員來表示每個屬性,但是這樣做有些浪費位。例如,只需1位即可表示方框是透明還是不透明;只需1位即可表示邊框是顯示還是隱藏。8種顏色可以用3位單元的8個可能的值來表示,而3種邊框樣式也只需2位單元即可表示。總共10位就足夠表示方框的5個屬性設置。

一種方案是:一個字節儲存方框內部(透明和填充色)的屬性,一個字節儲存方框邊框的屬性,每個字節中的空隙用未命名字段填充。struct box_props聲明如下:

struct box_props {

bool opaque : 1 ;

unsigned int fill_color: 3 ;

unsigned int: 4 ;

bool show_border  : 1 ;

unsigned int border_color : 3 ;

unsigned int border_style : 2 ;

unsigned int: 2 ;

};

加上未命名的字段,該結構共佔用 16 位。如果不使用填充,該結構佔用 10 位。但是要記住,C 以unsigned int作為位字段結構的基本佈局單元。因此,即使一個結構唯一的成員是1位字段,該結構的大小也是一個unsigned int類型的大小,unsigned int在我們的系統中是32位。另外,以上代碼假設C99新增的_Bool類型可用,在stdbool.h中,bool是_Bool的別名。

對於opaque成員,1表示方框不透明,0表示透明。show_border成員也用類似的方法。對於顏色,可以用簡單的RGB(即red-green-blue的縮寫)表示。這些顏色都是三原色的混合。顯示器通過混合紅、綠、藍像素來產生不同的顏色。在早期的計算機色彩中,每個像素都可以打開或關閉,所以可以使用用 1 位來表示三原色中每個二進制顏色的亮度。常用的順序是,左側位表示藍色亮度、中間位表示綠色亮度、右側位表示紅色亮度。表15.3列出了這8種可能的組合。fill_color成員和border_color成員可以使用這些組合。最後,border_style成員可以使用0、1、2來表示實線、點線和虛線樣式。

表15.3 簡單的顏色表示

程序清單15.3中的程序使用box_props結構,該程序用#define創建供結構成員使用的符號常量。注意,只打開一位即可表示三原色之一。其他顏色用三原色的組合來表示。例如,紫色由打開的藍色位和紅色位組成,所以,紫色可表示為BLUE|RED。

程序清單15.3 fields.c程序

/* fields.c -- 定義並使用字段 */

#include <stdio.h>

#include <stdbool.h>  // C99定義了bool、true、false

/* 線的樣式 */

#define SOLID  0

#define DOTTED 1

#define DASHED 2

/* 三原色 */

#define BLUE  4

#define GREEN  2

#define RED1

/* 混合色 */

#define BLACK  0

#define YELLOW (RED | GREEN)

#define MAGENTA (RED | BLUE)

#define CYAN  (GREEN | BLUE)

#define WHITE  (RED | GREEN | BLUE)

const char * colors[8] = { "black", "red", "green", "yellow",

"blue", "magenta", "cyan", "white" };

struct box_props {

bool opaque : 1;  // 或者 unsigned int (C99以前)

unsigned int fill_color : 3;

unsigned int : 4;

bool show_border : 1; // 或者 unsigned int (C99以前)

unsigned int border_color : 3;

unsigned int border_style : 2;

unsigned int : 2;

};

void show_settings(const struct box_props * pb);

int main(void)

{

/* 創建並初始化 box_props 結構 */

struct box_props box = { true, YELLOW, true, GREEN, DASHED };

printf("Original box settings:\n");

show_settings(&box);

box.opaque = false;

box.fill_color = WHITE;

box.border_color = MAGENTA;

box.border_style = SOLID;

printf("\nModified box settings:\n");

show_settings(&box);

return 0;

}

void show_settings(const struct box_props * pb)

{

printf("Box is %s.\n",

pb->opaque == true ? "opaque" : "transparent");

printf("The fill color is %s.\n", colors[pb->fill_color]);

printf("Border %s.\n",

pb->show_border == true ? "shown" : "not shown");

printf("The border color is %s.\n", colors[pb->border_color]);

printf("The border style is ");

switch (pb->border_style)

{

case SOLID: printf("solid.\n"); break;

case DOTTED: printf("dotted.\n"); break;

case DASHED: printf("dashed.\n"); break;

default: printf("unknown type.\n");

}

}

下面是該程序的輸出:

Original box settings:

Box is opaque.

The fill color is yellow.

Border shown.

The border color is green.

The border style is dashed.

Modified box settings:

Box is transparent.

The fill color is white.

Border shown.

The border color is magenta.

The border style is solid.

該程序要注意幾個要點。首先,初始化位字段結構與初始化普通結構的語法相同:

struct box_props box = {YES, YELLOW , YES, GREEN, DASHED};

類似地,也可以給位字段成員賦值:

box.fill_color = WHITE;

另外,switch語句中也可以使用位字段成員,甚至還可以把位字段成員用作數組的下標:

printf("The fill color is %s.\n", colors[pb->fill_color]);

注意,根據 colors 數組的定義,每個索引對應一個表示顏色的字符串,而每種顏色都把索引值作為該顏色的數值。例如,索引1對應字符串"red",枚舉常量red的值是1。

15.4.2 位字段和按位運算符

在同類型的編程問題中,位字段和按位運算符是兩種可替換的方法,用哪種方法都可以。例如,前面的例子中,使用和unsigned int類型大小相同的結構儲存圖形框的信息。也可使用unsigned int變量儲存相同的信息。如果不想用結構成員表示法來訪問不同的部分,也可以使用按位運算符來操作。一般而言,這種方法比較麻煩。接下來,我們來研究這兩種方法(程序中使用了這兩種方法,僅為了解釋它們的區別,我們並不鼓勵這樣做)。

可以通過一個聯合把結構方法和位方法放在一起。假定聲明了 struct box_props 類型,然後這樣聲明聯合:

union Views /* 把數據看作結構或unsigned short類型的變量 */

{

struct box_props st_view;

unsigned short us_view;

};

在某些系統中,unsigned int和box_props類型的結構都佔用16 位內存。但是,在其他系統中(例如我們使用的系統),unsigned int和box_props都是32位。無論哪種情況,通過聯合,都可以使用 st_view 成員把一塊內存看作是一個結構,或者使用 us_view 成員把相同的內存塊看作是一個unsigned short。結構的哪一個位字段與unsigned short中的哪一位對應?這取決於實現和硬件。下面的程序示例假設從字節的低階位端到高階位端載入結構。也就是說,結構中的第 1 個位字段對應計算機字的0號位(為簡化起見,圖15.3以16位單元演示了這種情況)。

圖15.3 作為整數和結構的聯合

程序清單15.4使用Views聯合來比較位字段和按位運算符這兩種方法。在該程序中,box是View聯合,所以box.st_view是一個使用位字段的box_props類型的結構,box.us_view把相同的數據看作是一個unsigned short 類型的變量。聯合只允許初始化第1 個成員,所以初始化值必須與結構相匹配。該程序分別通過兩個函數顯示 box 的屬性,一個函數接受一個結構,一個函數接受一個 unsigned short 類型的值。這兩種方法都能訪問數據,但是所用的技術不同。該程序還使用了本章前面定義的itobs函數,以二進制字符串形式顯示數據,以便讀者查看每個位的開閉情況。

程序清單15.4 dualview.c程序

/* dualview.c -- 位字段和按位運算符 */

#include <stdio.h>

#include <stdbool.h>

#include <limits.h>

/* 位字段符號常量 */

/* 邊框線樣式*/

#define SOLID 0

#define DOTTED1

#define DASHED2

/* 三原色 */

#define BLUE 4

#define GREEN 2

#define RED  1

/* 混合顏色 */

#define BLACK 0

#define YELLOW(RED | GREEN)

#define MAGENTA  (RED | BLUE)

#define CYAN (GREEN | BLUE)

#define WHITE (RED | GREEN | BLUE)

/* 按位方法中用到的符號常量 */

#define OPAQUE 0x1

#define FILL_BLUE 0x8

#define FILL_GREEN 0x4

#define FILL_RED  0x2

#define FILL_MASK 0xE

#define BORDER 0x100

#define BORDER_BLUE0x800

#define BORDER_GREEN  0x400

#define BORDER_RED0x 200

#define BORDER_MASK0xE00

#define B_SOLID0

#define B_DOTTED  0x1000

#define B_DASHED  0x2000

#define STYLE_MASK0x 3000

const char * colors[8] = { "black", "red", "green", "yellow", "blue", "magenta",

"cyan", "white" };

struct box_props {

bool opaque : 1;

unsigned int fill_color: 3;

unsigned int: 4;

bool show_border  : 1;

unsigned int border_color : 3;

unsigned int border_style : 2;

unsigned int: 2;

};

union Views /* 把數據看作結構或unsigned short類型的變量 */

{

struct box_props st_view;

unsigned short us_view;

};

void show_settings(const struct box_props * pb);

void show_settings1(unsigned short);

char * itobs(int n, char * ps);

int main(void)

{

/* 創建Views聯合,並初始化initialize struct box view */

union Views box = { { true, YELLOW, true, GREEN, DASHED } };

char bin_str[8 * sizeof(unsigned int) + 1];

printf("Original box settings:\n");

show_settings(&box.st_view);

printf("\nBox settings using unsigned int view:\n");

show_settings1(box.us_view);

printf("bits are %s\n",

itobs(box.us_view, bin_str));

box.us_view &= ~FILL_MASK;/* 把表示填充色的位清0 */

box.us_view |= (FILL_BLUE | FILL_GREEN);/* 重置填充色 */

box.us_view ^= OPAQUE;/* 切換是否透明的位 */

box.us_view |= BORDER_RED;/* 錯誤的方法 */

box.us_view &= ~STYLE_MASK;  /* 把樣式的位清0 */

box.us_view |= B_DOTTED; /* 把樣式設置為點 */

printf("\nModified box settings:\n");

show_settings(&box.st_view);

printf("\nBox settings using unsigned int view:\n");

show_settings1(box.us_view);

printf("bits are %s\n",

itobs(box.us_view, bin_str));

return 0;

}

void show_settings(const struct box_props * pb)

{

printf("Box is %s.\n",

pb->opaque == true ? "opaque" : "transparent");

printf("The fill color is %s.\n", colors[pb->fill_color]);

printf("Border %s.\n",

pb->show_border == true ? "shown" : "not shown");

printf("The border color is %s.\n", colors[pb->border_color]);

printf("The border style is ");

switch (pb->border_style)

{

case SOLID: printf("solid.\n"); break;

case DOTTED: printf("dotted.\n"); break;

case DASHED: printf("dashed.\n"); break;

default: printf("unknown type.\n");

}

}

void show_settings1(unsigned short us)

{

printf("box is %s.\n",

(us & OPAQUE) == OPAQUE ? "opaque" : "transparent");

printf("The fill color is %s.\n",

colors[(us >> 1) & 07]);

printf("Border %s.\n",

(us & BORDER) == BORDER ? "shown" : "not shown");

printf("The border style is ");

switch (us & STYLE_MASK)

{

case B_SOLID : printf("solid.\n"); break;

case B_DOTTED : printf("dotted.\n"); break;

case B_DASHED : printf("dashed.\n"); break;

default  : printf("unknown type.\n");

}

printf("The border color is %s.\n",

colors[(us >> 9) & 07]);

}

char * itobs(int n, char * ps)

{

int i;

const static int size = CHAR_BIT * sizeof(int);

for (i = size - 1; i >= 0; i--, n >>= 1)

ps[i] = (01 & n) + '0';

ps[size] = '\0';

return ps;

}

下面是該程序的輸出:

Original box settings:

Box is opaque.

The fill color is yellow.

Border shown.

The border color is green.

The border style is dashed.

Box settings using unsigned int view:

box is opaque.

The fill color is yellow.

Border shown.

The border style is dashed.

The border color is green.

bits are 00000000000000000010010100000111

Modified box settings:

Box is transparent.

The fill color is cyan.

Border shown.

The border color is yellow.

The border style is dotted.

Box settings using unsigned int view:

box is transparent.

The fill color is cyan.

Border not shown.

The border style is dotted.

The border color is yellow.

bits are 00000000000000000001011100001100

這裡要討論幾個要點。位字段視圖和按位視圖的區別是,按位視圖需要位置信息。例如,程序中使用BLUE表示藍色,該符號常量的數值為4。但是,由於結構排列數據的方式,實際儲存藍色設置的是3號位(位的編號從0開始,參見圖15.1),而且儲存邊框為藍色的設置是11號位。因此,該程序定義了一些新的符號常量:

#define FILL_BLUE 0x8

#define BORDER_BLUE0x800

這裡,0x8是3號位為1時的值,0x800是11號位為1時的值。可以使用第1個符號常量設置填充色的藍色位,用第2個符號常量設置邊框顏色的藍色位。用十六進制記數法更容易看出要設置二進制的哪一位,由於十六進制的每一位代表二進制的4位,那麼0x8的位組合是1000,而0x800的位組合是10000000000,0x800的位組合比0x8後面多8個0。但是以等價的十進制來看就沒那麼明顯,0x8是8,0x800是2048。

如果值是2的冪,那麼可以使用左移運算符來表示值。例如,可以用下面的#define分別替換上面的#define:

#define FILL_BLUE 1<<3

#define BORDER_BLUE1<<11

這裡,<<的右側是2的指數,也就是說,0x8是23,0x800是211。同樣,表達式1<<n指的是第n位為1的整數。1<<11是常量表達式,在編譯時求值。

可以使用枚舉代替#defined創建符號常量。例如,可以這樣做:

enum { OPAQUE = 0x1, FILL_BLUE = 0x8, FILL_GREEN = 0x4, FILL_RED = 0x2,

FILL_MASK = 0xE, BORDER = 0x100, BORDER_BLUE = 0x800,

BORDER_GREEN = 0x400, BORDER_RED = 0x200, BORDER_MASK = 0xE00,

B_DOTTED = 0x1000, B_DASHED = 0x2000, STYLE_MASK = 0x3000};

如果不想創建枚舉變量,就不用在聲明中使用標記。

注意,按位運算符改變設置更加複雜。例如,要設置填充色為青色。只打開藍色位和綠色位是不夠的:

box.us_view |= (FILL_BLUE | FILL_GREEN); /* 重置填充色 */

問題是該顏色還依賴於紅色位的設置。如果已經設置了該位(比如對於黃色),這行代碼保留了紅色位的設置,而且還設置了藍色位和綠色位,結果是產生白色。解決這個問題最簡單的方法是在設置新值前關閉所有的顏色位。因此,程序中使用了下面兩行代碼:

box.us_view &= ~FILL_MASK;/* 把表示填充色的位清0 */

box.us_view |= (FILL_BLUE | FILL_GREEN);/* 重置填充色 */

如果不先關閉所有的相關位,程序中演示了這種情況:

box.us_view |= BORDER_RED; /* 錯誤的方法 */

因為BORDER_GREEN位已經設置過了,所以結果顏色是BORDER_GREEN | BORDER_RED,被解釋為黃色。

這種情況下,位字段版本更簡單:

box.st_view.fill_color = CYAN; /*等價的位字段方法 */

這種方法不用先清空所有的位。而且,使用位字段成員時,可以為邊框和框內填充色使用相同的顏色值。但是用按位運算符的方法則要使用不同的值(這些值反映實際位的位置)。

其次,比較下面兩個打印語句:

printf("The border color is %s.\n", colors[pb->border_color]);

printf("The border color is %s.\n", colors[(us >> 9) & 07]);

第1條語句中,表達式pb->border_color的值在0~7的範圍內,所以該表達式可用作colors數組的索引。用按位運算符獲得相同的信息更加複雜。一種方法是使用ui>>9把邊框顏色右移至最右端(0號位~2號位),然後把該值與掩碼07組合,關閉除了最右端3位以外所有的位。這樣結果也在0~7的範圍內,可作為colors數組的索引。

警告

位字段和位的位置之間的相互對應因實現而異。例如,在早期的Macintosh PowerPC上運行程序清單15.4,輸出如下:

Original box settings:

Box is opaque.

The fill color is yellow.

Border shown.

The border color is green.

The border style is dashed.

Box settings using unsigned int view:

box is transparent.

The fill color is black.

Border not shown.

The border style is solid.

The border color is black.

bits are 10110000101010000000000000000000

Modified box settings:

Box is opaque.

The fill color is yellow.

Border shown.

The border color is green.

The border style is dashed.

Box settings using unsigned int view:

box is opaque.

The fill color is cyan.

Border shown.

The border style is dotted.

The border color is red.

bits are 10110000101010000001001000001101

該輸出的二進制位與程序示例15.4不同,Macintosh PowerPC把結構載入內存的方式不同。特別是,它把第1位字段載入最高階位,而不是最低階位。所以結構表示法儲存在前16位(與PC中的順序不同),而unsigned int表示法則儲存在後16位。因此,對於Macintosh,程序清單15.4中關於位的位置的假設是錯誤的,使用按位運算符改變透明設置和填充色設置時,也弄錯了位。

15.5 對齊特性(C11)

C11 的對齊特性比用位填充字節更自然,它們還代表了C在處理硬件相關問題上的能力。在這種上下文中,對齊指的是如何安排對像在內存中的位置。例如,為了效率最大化,系統可能要把一個 double 類型的值儲存在4 字節內存地址上,但卻允許把char儲存在任意地址。大部分程序員都對對齊不以為然。但是,有些情況又受益於對齊控制。例如,把數據從一個硬件位置轉移到另一個位置,或者調用指令同時操作多個數據項。

_Alignof運算符給出一個類型的對齊要求,在關鍵字_Alignof後面的圓括號中寫上類型名即可:

size_t d_align = _Alignof(float);

假設d_align的值是4,意思是float類型對象的對齊要求是4。也就是說,4是儲存該類型值相鄰地址的字節數。一般而言,對齊值都應該是2的非負整數次冪。較大的對齊值被稱為stricter或stronger,較小的對齊值被稱為weaker。

可以使用_Alignas 說明符指定一個變量或類型的對齊值。但是,不應該要求該值小於基本對齊值。例如,如果float類型的對齊要求是4,不要請求其對齊值是1或2。該說明符用作聲明的一部分,說明符後面的圓括號內包含對齊值或類型:

_Alignas(double) char c1;

_Alignas(8) char c2;

unsigned char _Alignas(long double) c_arr[sizeof(long double)];

注意

撰寫本書時,Clang(3.2版本)要求_Alignas(type)說明符在類型說明符後面,如上面第3行代碼所示。但是,無論_Alignas(type)說明符在類型說明符的前面還是後面,GCC 4.7.3都能識別,後來Clang 3.3 版本也支持了這兩種順序。

程序清單15.5中的程序演示了_Alignas和_Alignof的用法。

程序清單15.5 align.c程序

// align.c -- 使用 _Alignof 和 _Alignas (C11)

#include <stdio.h>

int main(void)

{

double dx;

char ca;

char cx;

double dz;

char cb;

char _Alignas(double) cz;

printf("char alignment:  %zd\n", _Alignof(char));

printf("double alignment: %zd\n", _Alignof(double));

printf("&dx: %p\n", &dx);

printf("&ca: %p\n", &ca);

printf("&cx: %p\n", &cx);

printf("&dz: %p\n", &dz);

printf("&cb: %p\n", &cb);

printf("&cz: %p\n", &cz);

return 0;

}

該程序的輸出如下:

char alignment: 1

double alignment: 8

&dx: 0x7fff5fbff660

&ca: 0x7fff5fbff65f

&cx: 0x7fff5fbff65e

&dz: 0x7fff5fbff650

&cb: 0x7fff5fbff64f

&cz: 0x7fff5fbff648

在我們的系統中,double的對齊值是8,這意味著地址的類型對齊可以被8整除。以0或8結尾的十六進制地址可被8整除。這就是地址常用兩個double類型的變量和char類型的變量cz(該變量是double對齊值)。因為char的對齊值是1,對於普通的char類型變量,編譯器可以使用任何地址。

在程序中包含 stdalign.h 頭文件後,就可以把 alignas 和 alignof 分別作為_Alignas 和_Alignof的別名。這樣做可以與C++關鍵字匹配。

C11在stdlib.h庫還添加了一個新的內存分配函數,用於對齊動態分配的內存。該函數的原型如下:

void *aligned_alloc(size_t alignment, size_t size);

第1個參數代表指定的對齊,第2個參數是所需的字節數,其值應是第1個參數的倍數。與其他內存分配函數一樣,要使用free函數釋放之前分配的內存。

15.6 關鍵概念

C 區別於許多高級語言的特性之一是訪問整數中單獨位的能力。該特性通常是與硬件設備和操作系統交互的關鍵。

C有兩種訪問位的方法。一種方法是通過按位運算符,另一種方法是在結構中創建位字段。

C11新增了檢查內存對齊要求的功能,而且可以指定比基本對齊值更大的對齊值。

通常(但不總是),使用這些特性的程序僅限於特定的硬件平台或操作系統,而且設計為不可移植的。

15.7 本章小結

計算硬件與二進制記數系統密不可分,因為二進制數的1和0可用於表示計算機內存和寄存器中位的開閉狀態。雖然C不允許以二進制形式書寫數字,但是它識別與二進制相關的八進制和十六進制記數法。正如每個二進制數字表示1位一樣,每個八進制位代表3位,每個十六進制位代表4位。這種關係使得二進制轉為八進制或十六進制較為簡單。

C 提供多種按位運算符,之所以稱為按位是因為它們單獨操作一個值中的每個位。~運算符將其運算對象的每一位取反,將1轉為0,0轉為1。按位與運算符(&)通過兩個運算對像形成一個值。如果兩運算對像中相同號位都為1,那麼該值中對應的位為1;否則,該位為0。按位或運算符(|)同樣通過兩個運算對像形成一個值。如果兩運算對像中相同號位有一個為1或都為1,那麼該值中對應的位為1;否則,該位為0。按位異或運算符(^)也有類似的操作,只有兩運算對像中相同號位有一個為1時,結果值中對應的位才為1。

C還有左移(<<)和右移(>>)運算符。這兩個運算符使位組合中的所有位都向左或向右移動指定數量的位,以形成一個新值。對於左移運算符,空出的位置設為 0。對於右移運算符,如果是無符號類型的值,空出的位設為0;如果是有符號類型的值,右移運算符的行為取決於實現。

可以在結構中使用位字段操控一個值中的單獨位或多組位。具體細節因實現而異。

可以使用_Alignas強制執行數據存儲區上的對齊要求。

這些位工具幫助C程序處理硬件問題,因此它們通常用於依賴實現的場合中。

15.8 複習題

複習題的參考答案在附錄A中。

1.把下面的十進制轉換為二進制:

a.3

b.13

c.59

d.119

2.將下面的二進制值轉換為十進制、八進制和十六進制的形式:

a.00010101

b.01010101

c.01001100

d.10011101

3.對下面的表達式求值,假設每個值都為8位:

a.~3

b.3 & 6

c.3 | 6

d.1 | 6

e.3 ^ 6

f.7 >> 1

g.7 << 2

4.對下面的表達式求值,假設每個值都為8位:

a.~0

b.!0

c.2 & 4

d.2 && 4

e.2 | 4

f.2 || 4

g.5 << 3

5.因為ASCII碼只使用最後7位,所以有時需要用掩碼關閉其他位,其相應的二進制掩碼是什麼?分別用十進制、八進制和十六進制來表示這個掩碼。

6.程序清單15.2中,可以把下面的代碼:

while (bits-- > 0)

{

mask |= bitval;

bitval <<= 1;

}

替換成:

while (bits-- > 0)

{

mask += bitval;

bitval *= 2;

}

程序照常工作。這是否意味著*=2等同於<<=1?+=是否等同於|=?

7.a.Tinkerbell計算機有一個硬件字節可讀入程序。該字節包含以下信息:

Tinkerbell和IBM PC一樣,從右往左填充結構位字段。創建一個適合存放這些信息的位字段模板。

b.Klinkerbell與Tinkerbell類似,但是它從左往右填充結構位字段。請為Klinkerbell創建一個相應的位字段模板。

15.9 編程練習

1.編寫一個函數,把二進制字符串轉換為一個數值。例如,有下面的語句:

char * pbin = "01001001";

那麼把pbin作為參數傳遞給該函數後,它應該返回一個int類型的值25。

2.編寫一個程序,通過命令行參數讀取兩個二進制字符串,對這兩個二進制數使用~運算符、&運算符、|運算符和^運算符,並以二進制字符串形式打印結果(如果無法使用命令行環境,可以通過交互式讓程序讀取字符串)。

3.編寫一個函數,接受一個 int 類型的參數,並返回該參數中打開位的數量。在一個程序中測試該函數。

4.編寫一個程序,接受兩個int類型的參數:一個是值;一個是位的位置。如果指定位的位置為1,該函數返回1;否則返回0。在一個程序中測試該函數。

5.編寫一個函數,把一個 unsigned int 類型值中的所有位向左旋轉指定數量的位。例如,rotate_l(x, 4)把x中所有位向左移動4個位置,而且從最左端移出的位會重新出現在右端。也就是說,把高階位移出的位放入低階位。在一個程序中測試該函數。

6.設計一個位字段結構以儲存下面的信息。

字體ID:0~255之間的一個數;

字體大小:0~127之間的一個數;

對齊:0~2之間的一個數,表示左對齊、居中、右對齊;

加粗:開(1)或閉(0);

斜體:開(1)或閉(0);

在一個程序中使用該結構來打印字體參數,並使用循環菜單來讓用戶改變參數。例如,該程序的一個運行示例如下:

該程序要使用按位與運算符(&)和合適的掩碼來把字體ID和字體大小信息轉換到指定的範圍內。

7.編寫一個與編程練習 6 功能相同的程序,使用 unsigned long 類型的變量儲存字體信息,並且使用按位運算符而不是位成員來管理這些信息。