結構(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 或記憶體配置時。

練習題

  1. 定義 struct circle { struct point center; double radius; }, 寫函式計算面積與周長。
  2. 寫 struct point midpoint(struct point a, struct point b):回傳兩點的中點。
  3. 寫 int rect_area(const struct rect *r),並確保它能處理「pt1/pt2 順序顛倒」的矩形 (提示:用 min/max 正規化)。
  4. 建立 struct student { char name[20]; int score; } 的陣列(至少 5 筆寫死資料), 寫函式找出最高分者並回傳其指標。
  5. 寫 void sort_by_score(struct student a[], int n):按分數由高到低排序結構陣列。
  6. (進階)比較 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 與位元欄位