本單元收齊 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 到檔案再由別的平台讀取,
結果不可預期。可攜格式請改用明確的位移/遮罩運算。
練習題
- 用
typedef定義Matrix3(double [3][3]的別名)並寫一個印矩陣的函式。 - 定義
enum weekday,寫函式int is_weekend(enum weekday d)。 - 用 enum 實作販賣機狀態機:
IDLE -> COIN_INSERTED -> DISPENSING -> IDLE, 以一串寫死的事件序列驅動並印出每步狀態。 - 用 union 觀察
double(64 位元)的位元表示:印出符號、11 位指數、52 位尾數。 - 設計位元欄位結構
struct rgb565 { unsigned b:5, g:6, r:5; }, 寫函式把 (r,g,b) 各 8 位元的顏色壓縮進去再解回來。 - (進階)為 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 家族與動態資料結構的入場券