本單元收齊 C 型別系統的幾件「特殊武器」: typedef 幫型別取別名、enum 定義一組具名常數、 union 讓多個成員共用同一塊記憶體、位元欄位(bit-field)以位元為單位切割儲存空間。 最後介紹把 enum 與 union 結合的 tagged union 模式——C 語言表達「多型資料」的標準手法。

學習目標

  • 會用 typedef 為結構、指標、函式指標等型別取別名,並了解命名慣例
  • 會定義 enum(含指定值),並用它實作簡單的狀態機
  • 理解 union 的「成員共用記憶體」語意,會用 union 觀察 float 的位元表示
  • 會宣告與使用位元欄位,了解其適用場景與不可移植之處
  • 掌握 tagged union 模式:用一個 tag 欄位記錄 union 目前存放的型別

對照 K&R 章節

本單元主題 K&R 2nd Edition
typedef §6.7 Typedef
union §6.8 Unions
位元欄位 §6.9 Bit-fields
enum §2.3 Constants(列舉常數)

觀念講解

1. typedef:型別別名

typedef 不創造新型別,只是取別名,讓程式更易讀、更易移植:

typedef int Length;                  /* Length 就是 int */
typedef char *String;                /* String 就是 char *(慣例上不建議藏指標) */

typedef struct point {
    int x, y;
} Point;                             /* 之後寫 Point 即可,不用 struct point */

Point p = { 1, 2 };

常見慣例: - 型別別名首字母大寫(K&R 風格:Length、Treeptr)或加 _t 後綴(POSIX 風格:size_t) - 把指標藏進 typedef(如 String)容易誤導讀者,多數風格指南不建議

2. enum:具名整數常數

enum month { JAN = 1, FEB, MAR, APR };   /* FEB=2, MAR=3 ... 自動遞增 */
enum flag  { OFF, ON };                  /* 不指定就從 0 開始 */

比起一堆 #define,enum 的優點:值自動編號、是真正的識別字(除錯器看得到)、 可以宣告變數 enum month m = JAN;。

enum 當狀態機是最實用的模式之一:

enum state { IDLE, RUNNING, DONE };
enum state s = IDLE;
switch (s) {
case IDLE:    /* ... */ break;
case RUNNING: /* ... */ break;
case DONE:    /* ... */ break;
}   /* 開著 -Wall 時,漏掉 case 編譯器會警告——這是 enum 的隱藏福利 */

3. union:成員共用記憶體

union 的所有成員從同一個位址開始,大小等於最大成員(加對齊):

union value {
    int   i;
    float f;
    char  bytes[4];
};
struct(成員各有空間)        union(成員疊在一起)
+-----+-----+-----+          +---------------+
|  i  |  f  |bytes|          | i / f / bytes |  <- 同一塊記憶體
+-----+-----+-----+          +---------------+
sizeof = 12+                  sizeof = 4

規則:寫入哪個成員,就應該讀哪個成員。 (例外:C11 允許透過 union 做「型別雙關(type punning)」重新解讀位元, 這正是用 union 觀察 float 位元表示的原理。)

4. 用 union 看 float 的位元

IEEE 754 單精度浮點數 = 1 個符號位 + 8 個指數位 + 23 個尾數位:

union { float f; unsigned int u; } pun;
pun.f = -6.25f;
printf("%08x\n", pun.u);    /* 印出 float 的原始位元 */
 31 30      23 22                    0
+--+----------+-----------------------+
|S |  E (8)   |       M (23)          |
+--+----------+-----------------------+

5. 位元欄位(bit-fields)

在結構中以「位元」為單位宣告成員,適合旗標集合與封包格式:

struct flags {
    unsigned int is_keyword : 1;   /* 只占 1 個位元 */
    unsigned int is_extern  : 1;
    unsigned int level      : 4;   /* 0..15 */
};

注意事項(K&R 也強調): - 欄位的排列順序、是否跨越單位邊界都依實作而定——不可移植 - 位元欄位沒有位址,不能對它取 & - 宣告用 unsigned int 或 int(建議一律 unsigned,避免符號問題)

6. tagged union:C 的「變體型別」

union 本身不記得「現在存的是哪個成員」,所以標準做法是配一個 enum tag:

enum shape_kind { SHAPE_CIRCLE, SHAPE_RECT };

struct shape {
    enum shape_kind kind;      /* tag:記錄 union 的當前身分 */
    union {
        struct { double r; } circle;
        struct { double w, h; } rect;
    } u;
};

讀取前先看 tag,switch (s->kind) 分派——這就是 K&R §6.8 說的 「由程式自己維護目前存放的型別」的紀律化版本。

範例程式

檔案 內容
01_typedef_enum.c typedef 各種用法(結構/陣列/函式指標)、enum 基礎、enum 狀態機:交通號誌模擬
02_union_float_bits.c union 佈局觀察、用 union 拆解 float 的符號/指數/尾數、位元組序觀察
03_bitfields_tagged_union.c 位元欄位封裝旗標與日期、tagged union 實作 shape 面積計算

編譯與執行(以第一個為例):

cc -std=c11 -Wall -Wextra -o /tmp/demo 01_typedef_enum.c
/tmp/demo

常見錯誤與陷阱

1. 讀取 union 中「不是最後寫入」的成員卻期望有意義的值

union { int i; float f; } v;
v.f = 3.14f;
printf("%d\n", v.i);   /* 印出的是 3.14f 的位元重新解讀,不是 3 */

若你要的是轉換,用 (int)v.f;若你要的就是位元重解讀,那沒問題——但要知道自己在做什麼。

2. union 忘記配 tag,事後不知道存的是什麼

union value v;
/* ... 幾百行之後 ... */
printf("%d\n", v.i);   /* 誰知道現在裡面是 int 還是 float? */

修正:一律使用 tagged union 模式,讀取前 switch (tag)。

3. 對位元欄位取位址

struct flags f;
scanf("%u", &f.level);   /* 編譯錯誤:位元欄位沒有位址 */

修正:先讀到普通變數,再指派給位元欄位。

4. 假設 enum 變數只會是列出的值

enum color { RED, GREEN, BLUE };
enum color c = 42;       /* C 不會阻止你!enum 底層就是整數 */

在 switch 中保留 default: 處理意外值,或在邊界處驗證輸入。

5. 依賴位元欄位的記憶體佈局做跨平台傳輸

位元欄位的排列由編譯器決定,直接把含位元欄位的結構 fwrite 到檔案再由別的平台讀取, 結果不可預期。可攜格式請改用明確的位移/遮罩運算。

練習題

  1. 用 typedef 定義 Matrix3(double [3][3] 的別名)並寫一個印矩陣的函式。
  2. 定義 enum weekday,寫函式 int is_weekend(enum weekday d)。
  3. 用 enum 實作販賣機狀態機:IDLE -> COIN_INSERTED -> DISPENSING -> IDLE, 以一串寫死的事件序列驅動並印出每步狀態。
  4. 用 union 觀察 double(64 位元)的位元表示:印出符號、11 位指數、52 位尾數。
  5. 設計位元欄位結構 struct rgb565 { unsigned b:5, g:6, r:5; }, 寫函式把 (r,g,b) 各 8 位元的顏色壓縮進去再解回來。
  6. (進階)為 tagged union struct json_value(支援 number/string/bool/null) 寫一個 print_json_value 函式。

延伸閱讀

  • K&R 2nd Edition,§6.7–6.9、§2.3(本單元主軸)
  • IEEE 754 標準概觀:https://en.wikipedia.org/wiki/IEEE_754
  • 《What Every Computer Scientist Should Know About Floating-Point Arithmetic》
  • 下一單元 07-dynamic-memory:malloc 家族與動態資料結構的入場券