結構(struct)讓你把「相關的資料」打包成一個單位——例如一個點的 x、y 座標,
一個員工的姓名、編號與薪水。結構是 C 建構資料抽象的基石,
之後的鏈結串列、樹、以及幾乎所有真實世界的 C 程式都建立在它之上。
學習目標
- 會宣告結構型別、定義結構變數並初始化(含 C99 指定初始化器)
- 熟練成員存取的兩種運算子:
.(結構本身)與->(透過指標) - 理解結構「傳值 vs 傳指標」給函式的差異與選擇原則
- 會操作結構陣列與巢狀結構
- 初步理解記憶體對齊(alignment)與填補(padding)
- 看懂 K&R 的點(point)與矩形(rect)範例
對照 K&R 章節
| 本單元主題 | K&R 2nd Edition |
|---|---|
| 結構基礎(point 範例) | §6.1 Basics of Structures |
| 結構與函式 | §6.2 Structures and Functions |
| 結構陣列 | §6.3 Arrays of Structures |
| 指向結構的指標 | §6.4 Pointers to Structures |
觀念講解
1. 宣告、定義與初始化
/* 宣告一個「結構模板」:本身不配置記憶體 */
struct point {
int x;
int y;
};
/* 定義變數(配置記憶體)並初始化 */
struct point pt = { 320, 200 };
/* C99 指定初始化器:順序無關、漏掉的成員自動為 0,推薦! */
struct point origin = { .x = 0, .y = 0 };
struct point 整體才是型別名稱;point 這個「標籤(tag)」不能單獨使用
(除非用 typedef,見下一單元)。
2. 成員存取:. 與 ->
struct point pt = { 3, 4 };
struct point *pp = &pt;
pt.x = 10; /* 結構變數用 . */
pp->y = 20; /* 結構指標用 ->,等同 (*pp).y */
(*pp).x = 30; /* 合法但囉嗦:-> 就是它的縮寫 */
-> 存在的理由:*pp.y 會被解析成 *(pp.y)(. 優先權較高),
必須寫 (*pp).y,太醜了,所以發明了 pp->y。
3. 巢狀結構(K&R 矩形範例)
struct rect {
struct point pt1; /* 左下角 */
struct point pt2; /* 右上角 */
};
struct rect screen = { { 0, 0 }, { 1024, 768 } };
int width = screen.pt2.x - screen.pt1.x; /* 成員鏈一路點下去 */
4. 結構與函式:傳值 vs 傳指標
結構可以整個傳值(複製一份)也可以傳指標:
傳值 f(struct point p) |
傳指標 f(const struct point *p) |
|
|---|---|---|
| 複製成本 | 複製整個結構 | 只複製 8 bytes 指標 |
| 能否改到本尊 | 不能(隔離、安全) | 能(要修改時必用) |
| 適用 | 小結構(如 point) | 大結構、需要修改時 |
慣例:小結構傳值讓語意乾淨(K&R 的 makepoint、addpoint 都是傳值);
大結構傳 const 指標省複製;需要修改就傳普通指標。
/* K&R §6.2:用回傳值建構結構 */
struct point makepoint(int x, int y)
{
struct point p = { x, y };
return p; /* 結構可以整個回傳 */
}
5. 結構陣列
struct keyword {
const char *word;
int count;
};
struct keyword keytab[] = { /* K&R §6.3 關鍵字計數表的縮影 */
{ "break", 0 },
{ "case", 0 },
{ "int", 0 },
};
int nkeys = sizeof keytab / sizeof keytab[0]; /* 元素個數的慣用算法 */
走訪結構陣列時,p++ 會前進「一整個結構」的大小——指標算術自動處理。
6. 記憶體對齊與 padding
CPU 存取對齊的資料較快(有些架構甚至強制對齊), 編譯器會在成員之間插入填補位元組(padding):
struct s1 { char c; int i; }; /* 不是 5 bytes! */
offset: 0 1 2 3 4 5 6 7
+-----+-----+-----+-----+-----+-----+-----+-----+
| c | pad | pad | pad | i |
+-----+-----+-----+-----+-----+-----+-----+-----+
sizeof(struct s1) == 8
實用結論:
- sizeof(struct) ≥ 成員大小總和,不要用手加的方式猜大小
- 把成員由大到小排列通常能減少 padding
- 用 offsetof(struct s1, i)(<stddef.h>)可查詢成員的實際位移
範例程式
| 檔案 | 內容 |
|---|---|
01_point_rect.c |
K&R 點與矩形:宣告/初始化/巢狀結構、makepoint/addpoint/ptinrect |
02_struct_functions.c |
傳值 vs 傳指標的行為對比、-> 運算子、用 const 指標避免複製 |
03_struct_array_padding.c |
結構陣列(員工資料庫小範例:搜尋/統計/排序)、padding 與 offsetof 觀察 |
編譯與執行(以第一個為例):
cc -std=c11 -Wall -Wextra -o /tmp/demo 01_point_rect.c
/tmp/demo
常見錯誤與陷阱
1. 宣告結構模板時忘記分號
struct point {
int x, y;
} /* <- 少了分號,錯誤會報在下一行,很難找 */
int main(void) { }
修正:};。這個錯誤的症狀常是「main 的型別很奇怪」之類的怪訊息。
2. 對結構指標用 . 或對結構用 ->
struct point *pp = &pt;
pp.x = 1; /* 錯:pp 是指標,要用 pp->x */
pt->y = 2; /* 錯:pt 是結構本身,要用 pt.y */
記法:有星號(指標)就用箭頭。
3. 以為傳值函式能修改結構
void move(struct point p) { p.x += 10; } /* 改的是複本 */
move(pt); /* pt 完全沒變 */
修正:傳指標 void move(struct point *p) { p->x += 10; },呼叫 move(&pt);。
4. 用 == 比較兩個結構
if (a == b) { ... } /* 編譯錯誤:結構沒有內建的 == */
修正:逐成員比較,自己寫 int point_eq(struct point a, struct point b)。
(memcmp 不可靠——padding 的內容是未定義的。)
5. 假設結構大小等於成員總和
struct s { char c; int i; };
/* 錯誤假設 sizeof == 5,實際是 8(含 padding) */
永遠用 sizeof 取得真實大小,尤其是做二進位 I/O 或記憶體配置時。
練習題
- 定義
struct circle { struct point center; double radius; }, 寫函式計算面積與周長。 - 寫
struct point midpoint(struct point a, struct point b):回傳兩點的中點。 - 寫
int rect_area(const struct rect *r),並確保它能處理「pt1/pt2 順序顛倒」的矩形 (提示:用 min/max 正規化)。 - 建立
struct student { char name[20]; int score; }的陣列(至少 5 筆寫死資料), 寫函式找出最高分者並回傳其指標。 - 寫
void sort_by_score(struct student a[], int n):按分數由高到低排序結構陣列。 - (進階)比較
struct { char a; int b; char c; }與struct { int b; char a; char c; }的sizeof,用offsetof印出每個成員的位移並解釋差異。
延伸閱讀
- K&R 2nd Edition,§6.1–6.4(本單元主軸)
- K&R §6.3 完整的關鍵字計數程式(結構陣列 + 二分搜尋)
- 《The Lost Art of Structure Packing》by Eric S. Raymond(padding 的深入好文)
- 下一單元
06-unions-enums-bitfields:typedef、enum、union 與位元欄位