第十三章 其他的C語言課題
13- 1 位元資料的處理 13- 2 位元欄位 (Bit Field) 13- 3 列舉型態 (enumeration type) 13- 4 自定型態 (typedef)
13-1 位元資料的處理 宣告一筆八位元的資料 unsigned char bdata; bit 7 6 5 4 3 2 1 0 13-1 位元資料的處理 宣告一筆八位元的資料 unsigned char bdata; bit 7 6 5 4 3 2 1 0 ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─┴─┴─┴─┴─┘ 運算符號 功 能 ───────────────────────── & 位元 AND | 位元 OR ^ 位元 XOR N 取 1 的補數(即 0 變 1,1 變 0) << 左移 >> 右移
┌───────┐ │ &(位元 AND)│ └───────┘ & 運算是將左、右兩邊的運算元看成是位元資料,對每個位元做 AND 運算,也就是兩個位元都是 1 時,結果才是 1。 AND │ 0 1 ───┼───── 0 │ 0 0 1 │ 0 1 (AND 真值表)
例如 unsigned char bdata = 0xCC, Cdata = 0xAA; unsigned char x; x = bdata & chata; 進行運算 1100 1100 (0xCC) bdata &) 1010 1010 (0xAA) cdata ────────────────── 1000 1000 (0x88) 所以 x 的值為 0x88。
再看 bdata = bdata & 0x80; 的結果 1100 1100 (0xCC) &) 1000 1000 (0x80) ───────────── 1000 1000 我們觀察到因為 0x80 的 bit 7 是 1 其餘 bit 是 0,所以 & 的結果只有 bdata 的 bit 7 保留下來,bdata 的其餘 bit 都被清除為0。據此,只要和 0x80 做 & 運算,根據結果值是不是 0x00(也就是數值 0),就可得知原先 bdata 的 bit 7 是 0 還是 1。 & 運算常用於清除位元資料的某些位元,例如 cdata = cdata & 0xF8; 會把 cdata 的 bit0、bit1 及 bit2 清除為0,其餘位元則不變動。
┌───────┐ │ |(位元 OR) │ └───────┘ | 運算是將左右兩方的位元資料,對每個位元進行 OR 運算,也就是兩個位元中有一個以上是 1 是,結果就是 1。 例如,0xCC | 0xAA 的結果為 0xEE 1100 1100 (0xCC) OR │ 0 1 |) 1010 1010 (0xAA) ───┼───── ─────────── 0 │ 0 1 1110 1110 1 │ 1 1 (OR 真值表)
我們可以用 | 運算,來將位元資料的某些位元,設定成 1。 例如, bdata = bdata | 0x84; ┌─┬─┬─┬─┬─┬─┬─┬─┐ │x │ x │ x │ x │ x │ x │x │ x │ (bdata) └─┴─┴─┴─┴─┴─┴─┴─┘ |) 1 0 0 0 0 1 0 0 (0x84) ───────────────────── 1 x x x x 1 x x (x 表示 0 或 1) 可以將 bdata 的 bit2 和 bit7 設定成 1,其餘 bit 保持不變
┌───────┐ │ ^(位元 XOR)│ └───────┘ ^ 運算是將左右兩邊的運算元,對每個位元進行XOR (Exclusive OR) 運算,也就是所謂 "不進位的加法"。XOR 運算的真值表如下所示: XOR │ 0 1 ───┼───── 0 │ 0 1 1 │ 1 0
例如,bdata 值為 0xCC,cdata 值為 0xAA, 若 adata = bdata ^ cdata; 則 adata 的值應為 0x66。 1100 1100 ^) 1010 1010 ──────── 0110 0110 有趣的是,如果再將 adata 與 cdata 做 ^ 運算,則會得到 bdata的值,也就是 (bdata ^ cdata) ^ cdata的結果仍舊是 bdata。
┌────────┐ │ <<(位元左移) │ └────────┘ << 運算是將左邊運算元的各位元都向左移數個位元,移動的位元數由右邊運算元來決定。例如,adata 的值為 0xCC,則bdata = adata << 2;會得到 bdata 的值為 0x30 ┌─┬─┬─┬─┬─┬─┬─┬─┐ │1 │1 │0 │0 │1 │1 │0 │0 │ 0xCC └─┴─┴─┴─┴─┴─┴─┴─┘ 左移 2 位元 1 1 │0 │0 │1 │1 │0 │0 │0 │ └─┘ └─ ─┘ 捨 棄 補 0
我們使用 << 運算來將某些位元向左移到較高的位元位置,以便進一步的處理。另一種常見的左移運算是用於整數資料的處理,如果不管溢位的話,左移 2 個位元,就相當於乘上 2 的平方,也就是乘以 4。經常會有人使用下列寫法 bdata = bdata << 8; 這相當於 bdata = bdata * 256; 為什麼不直接乘呢?因為 "乘法" 在電腦計算較費時,而 "左移" 運算則執行較快速。不過,實際使用時,應注意整數資料 bdata的有效數值範圍,來宣告適當的資料型態。
┌────────┐ │ >>(位元右移) │ └────────┘ >> 運算和 << 運算類似,作用是將位元資料向右移數個位元。例如,bdata = bdata >> num; 是將 bdata 的值向右移 num 個位元後,取代舊的值。 對整數資料而言,每右移一個位元就相當於除以 2 的效果。
【範例 13-1-1】 4 #define BIT0 0x01 5 #define BIT3 0x08 6 #define BIT7 0x80 10 unsigned char bt=0xAA; 11 unsigned int wd=0x8FFF; 12 13 printf("%X\n",bt); 14 printf("Set Bit 0 : %X \n",bt|BIT0); 15 printf("Clear Bit 7 : %X \n",bt&(~BIT7)); 16 printf("Check Bit 3 : is %s \n",(bt & BIT3) ? "ON" : "OFF" ); 17 printf("Low byte of wd is %X \n", wd&0x00FF); 18 printf("High byte of wd is %X \n",wd>>8); 19 printf("Toggle of wd is %X \n",wd^0xFFFF); 20 }
執行結果: AA Set Bit 0 : AB Clear Bit 7 : 2A Check Bit 3 : is ON Low byte of wd is FF High byte of wd is 8F Toggle of wd is 7000
13-2 位元欄位(Bit Field) 當記憶體空間有限時,需要想辦法把數個簡單的資料,合併儲存在同一個位元組之中。例如將幾個位元旗標 (flag) 放在同一字元內,在一些控制硬體界面的應用中,常用這種方式儲存某些事件的狀態。 一個常見的例子就是鍵盤按鍵狀態的檢查。如果指定一個 unsignedint 的變數 flags,來儲存這些狀態,例如 bit 2 是檢查 Alt 鍵是否按下,bit 1 代表 Ctrl 鍵是否按下,而 bit 0 代表 shift 鍵的狀態。 一般的做法是就每一個位元,定義一個位元遮罩 (bit mask),其數值剛好是 2 的幕次,例如: #define SHIFT 0x01 #define CTRL 0x02 #define ALT 0x04
當我們要將 CTRL 及 SHIFT 旗標設定為 1 時,可使用 flags |= CTRL | SHIFT; 如果需要清除 CTRL 及 SHIFT 旗標,則使用 flags &= ~(CTRL | SHIFT); 想要檢查 CTRL、SHIFT 或 ALT 是否有任何一個被設定為 1,可使用下列方式: if ((flags & (CTRL | SHIFT | ALT))==0)
除了上述的位元資料處理方式之外,C 語言也提供了位元欄位 (bit field) 的表示方式,也就是說可以指定一個結構的欄位是位元資料。例如上述的位元遮罩可以使用結構來定義: struct { unsigned int ctrl_flag : 1; unsigned int shift_flag : 1; unsigned int alt_flag : 1; } flags; 這樣定義了一個由 3 個位元欄位所構成的變數 flags ,:1指定了各個欄位都佔一個位元,冒號後面的整數代表了這項欄位資料
設定 SHIFT 與 CTRL 旗標的動作分別變成, flags.shift_flag = 1; flags.ctrl_flag = 1; 將 SHIFT 與 CTRL 旗標全部清除,可直接使用 flags.shift_flag = flags.ctrl_flag = 0; 判別 ALT 鍵是否被按下,則直接使用 if (flags.alt_flag == 1)
13-3 列舉型態 (enumeration type) 列舉型態 (enumeration type) 讓使用者很方便地定義一些相關的常數值,這種賦與一連串的常數值特定的名稱,比起使用數個 #define 來定義這個常數名稱更簡潔有力。此外,列舉型態的使用,可讓程式除錯時,獲得較佳的訊息。 列舉常數的宣告格式如下: enum 標籤名稱 {常數名稱 1,常數名稱 2,…常數名稱 N} 其中的 enum 是保留字,宣告列舉型態;標籤名稱是指定這些常數的集體類型,大括弧內包含所有的常數名稱,它們都必須是整數常數。如果常數名稱後面沒有加等號 = 來設定其值,那麼就從 0 開始排起,依序加1 ,做為列舉常數的值。例如,
enum boolean {NO,YES}; enum logical {FALSE, TRUE}; enum weekdays {SUN = 1, MON, TUE, WES, THR, FRI, SAT}; enum escapes {BELL = '\a', BCAKSPACE = '\b',TAB = '\t', NEWLINE = '\n',VTAB = '\v',RETURN = '\r'};
13-4自定型態 (typedef) C語言可以讓程式設計者自己定義新的資料型態,例如: typedef char *String; char *name, *p, *lineptr[100]; 換成 String name, p, lineptr[100]; 這樣可以更清楚地表示 name,p 等變數是處理有關 "字串" 資料的意思,而 String 就可以當成一般的資料型態,用於宣告相關的變數。
typedef char *BUFFER; 定義 BUFFER 是 (char *) 的型態雖然它和上述的 String 是指向字元資料的指標,但是我們較容易分辨出下列宣告: String address; BUFFER grapharea; address是 "字串" 意思的字元指標,而 grapharea 是儲存字元資料的 "緩衝區",藉此增加程式可讀性。
至於較複雜的範例,多用於動態的資料結構。例如, typedef struct node { char line data[81]; struct node *next; } Listnode; Listnode n, *p; 定義了一種新的資料型態稱為 Listnode,它是一組結構包括一個 81個字元的欄位,以及指向另一節點的指標欄,而變數 n 是一個 Listnode