本檔收錄本單元的練習題、完整解題思路與參考解答。 強烈建議先自己動手做:看完題目就闔上這份檔案,寫出你的版本、編譯、執行、卡住了再回來看「解題思路」; 真的做完了,再對照「參考解答」與「程式碼講解」。每題的完整程式碼也放在 exercises/ 資料夾,可直接編譯:

cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_circle.c && /tmp/out

練習 1:struct circle —— 巢狀結構與面積、周長(難度:基礎)

題目

定義 struct circle { struct point center; double radius; }(struct point 含 int x, y), 寫兩個函式計算圓的面積(πr²)與周長(2πr)。 用「圓心 (3,4)、半徑 5」與「單位圓」測試——單位圓的面積應接近 π、周長應接近 2π。

解題思路

  1. 資料表示:一個圓由「圓心位置」與「半徑」完全決定。 圓心本身就是一個點——直接把 struct point 當成員(巢狀結構), 而不是把 x、y、radius 攤平在同一層。這樣點的相關操作(之後可能有)可以復用。
  2. 成員鏈的存取:c1.center.x——. 運算子從外往內一路點下去。
  3. 初始化:用 C99 指定初始化器最清楚: { .center = { .x = 3, .y = 4 }, .radius = 5.0 }——順序無關、一眼看懂哪個值是什麼。
  4. 函式介面選擇:struct circle 只有 16 bytes 左右,傳值即可 (K&R 的小結構慣例);傳值的好處是函式絕不可能改到呼叫端的圓。
  5. 公式直翻:面積 PI * r * r、周長 2 * PI * r。 PI 用 #define 給足位數(C 標準庫沒有內建 π)。
  6. 驗證手段:單位圓(r=1)的面積就是 π 本人、周長就是 2π—— 拿「已知答案」的輸入當測資,是數值程式的基本測試技巧。

參考解答

/*
 * ex1_circle.c
 * ------------
 * 練習 1:struct circle —— 巢狀結構與面積、周長計算(難度:基礎)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex1_circle.c
 * 執行:/tmp/out
 */
#include <stdio.h>

#define PI 3.14159265358979323846

struct point {
    int x;
    int y;
};

/* 巢狀結構:圓 = 圓心(一個 point)+ 半徑 */
struct circle {
    struct point center;
    double radius;
};

/* 面積 = πr²;小結構傳值即可(複製成本低、語意乾淨) */
double circle_area(struct circle c)
{
    return PI * c.radius * c.radius;
}

/* 周長 = 2πr */
double circle_circumference(struct circle c)
{
    return 2.0 * PI * c.radius;
}

int main(void)
{
    /* C99 指定初始化器:順序無關、可讀性高 */
    struct circle c1 = { .center = { .x = 3, .y = 4 }, .radius = 5.0 };
    struct circle unit = { .center = { 0, 0 }, .radius = 1.0 };

    printf("c1:圓心 (%d, %d),半徑 %.1f\n",
           c1.center.x, c1.center.y, c1.radius);
    printf("  面積 = %.4f\n", circle_area(c1));
    printf("  周長 = %.4f\n", circle_circumference(c1));

    printf("單位圓:\n");
    printf("  面積 = %.4f(應接近 π)\n", circle_area(unit));
    printf("  周長 = %.4f(應接近 2π)\n", circle_circumference(unit));

    return 0;
}

程式碼講解

  • struct circle 的定義必須出現在 struct point 之後—— 編譯器要知道成員 center 佔多大空間,前向宣告在這裡不夠用。
  • c1.center.x:兩層 .,由外而內。若手上是指標 struct circle *pc, 則寫 pc->center.x(第一層用箭頭、第二層 center 是結構本體所以用點)。
  • 面積函式用 c.radius * c.radius 而非 pow(c.radius, 2): 不必為平方拉進 <math.h> 與 -lm。
  • 易錯處:結構定義結尾的分號(};)忘了會產生一連串莫名其妙的編譯錯誤, 錯誤訊息常指向下一行的函式。

範例輸出

c1:圓心 (3, 4),半徑 5.0
  面積 = 78.5398
  周長 = 31.4159
單位圓:
  面積 = 3.1416(應接近 π)
  周長 = 6.2832(應接近 2π)

練習 2:midpoint —— 回傳兩點的中點(難度:基礎)

題目

寫函式 struct point midpoint(struct point a, struct point b):回傳兩點的中點 (K&R makepoint 的風格:參數傳值、整個結構當回傳值)。 測試一般情況與「座標和為奇數」的情況(整數除法會取整), 並示範函式呼叫的串接:midpoint(a, midpoint(a, b)) 算四分之一點。

解題思路

  1. 介面模仿 K&R §6.2:addpoint(struct point p1, struct point p2) 整個結構進、 整個結構出。中點函式照這個模子刻。
  2. 數學:中點座標 = 兩端點座標的平均:mid.x = (a.x + b.x) / 2;。
  3. 注意整數除法:struct point 的成員是 int,(0 + 5) / 2 得 2 不是 2.5。 這是資料表示的取捨:座標若需要精度應改用 double 成員; 本題保留 int 並在測試中明確展示取整行為,讓行為「可預期」。
  4. 結構回傳的機制:return mid; 會把整個結構複製給呼叫端。 區域變數 mid 雖然在函式結束後消失,但複製發生在消失之前——完全安全 (對比:回傳區域變數的指標才是懸空指標錯誤)。
  5. 串接呼叫:因為回傳的是值(不是指標),midpoint(a, midpoint(a, b)) 可以像數學函數一樣組合,這是「傳值風格」最漂亮的地方。

參考解答

/*
 * ex2_midpoint.c
 * --------------
 * 練習 2:midpoint —— 回傳兩點的中點(難度:基礎)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex2_midpoint.c
 * 執行:/tmp/out
 */
#include <stdio.h>

struct point {
    int x;
    int y;
};

/*
 * K&R §6.2 makepoint 的風格:參數傳值、整個結構當回傳值。
 * 中點 = 兩點座標的平均(int 相除會取整,這是本題要注意的地方)。
 */
struct point midpoint(struct point a, struct point b)
{
    struct point mid;
    mid.x = (a.x + b.x) / 2;
    mid.y = (a.y + b.y) / 2;
    return mid;              /* 結構可以整個回傳(複製給呼叫端) */
}

static void show(const char *label, struct point p)
{
    printf("%s = (%d, %d)\n", label, p.x, p.y);
}

int main(void)
{
    struct point a = { 0, 0 };
    struct point b = { 10, 20 };
    struct point c = { -3, 7 };

    show("midpoint((0,0), (10,20))", midpoint(a, b));
    show("midpoint((10,20), (-3,7))", midpoint(b, c));

    /* 奇數和:整數除法向零取整((0+5)/2 = 2) */
    struct point d = { 5, 5 };
    show("midpoint((0,0), (5,5))  ", midpoint(a, d));

    /* 函式呼叫可以直接串接:中點的中點 */
    show("四分之一點            ", midpoint(a, midpoint(a, b)));

    return 0;
}

程式碼講解

  • return mid;:回傳的是複本。呼叫端 show(..., midpoint(a, b)) 中, 回傳值直接當引數再傳值一次——結構在 C 裡是「一等公民的值」。
  • (a.x + b.x) / 2:兩個很大的 int 相加可能溢位;教學情境的座標範圍內安全, 嚴謹寫法是 a.x + (b.x - a.x) / 2(先取差再折半,不易溢位)。
  • midpoint(a, midpoint(a, b)):內層回傳 (5,10),外層算 (0,0) 與 (5,10) 的中點 → (2,5)。注意 x 座標 5/2 取整為 2——輸出印證了整數除法。
  • 易錯處:想在函式內 a.x = ... 修改呼叫端的點——改的是複本。 要修改就得傳指標(下一題就是)。

範例輸出

midpoint((0,0), (10,20)) = (5, 10)
midpoint((10,20), (-3,7)) = (3, 13)
midpoint((0,0), (5,5))   = (2, 2)
四分之一點             = (2, 5)

練習 3:rect_area —— 處理頂點順序顛倒的矩形(難度:基礎)

題目

寫函式 int rect_area(const struct rect *r) 計算矩形面積, 並確保它能處理「pt1/pt2 順序顛倒」的矩形(提示:用 min/max 正規化)。 測試四種情況:pt1 左下 pt2 右上(標準)、完全顛倒、左上/右下交錯、退化成線段。

解題思路

  1. 先識別問題:天真的 (pt2.x - pt1.x) * (pt2.y - pt1.y) 只在 「pt1 是左下、pt2 是右上」時正確。頂點顛倒時寬或高變負數: - 完全顛倒:負 × 負 = 正(僥倖對); - 左上/右下交錯:正 × 負 = 負面積(錯!)。 所以「碰巧測一組資料通過」不代表正確——要窮舉四種相對位置。
  2. 正規化:寬 = x 座標的最大值 − 最小值,高同理。 imax(x1,x2) - imin(x1,x2) 等價於 abs(x1-x2),但 min/max 寫法 更清楚表達「先正規化再相減」的意圖,也順手寫了兩個小工具函式。
  3. 介面選擇:struct rect 含兩個點共 16 bytes,仍不大, 但這題刻意用 const 指標示範另一種慣例:不複製、且承諾不修改 (-> 運算子上場)。
  4. 退化情況:兩點同一水平線 → 高為 0 → 面積 0。數學上自然成立,不需特判。

參考解答

/*
 * ex3_rect_area.c
 * ---------------
 * 練習 3:rect_area —— 能處理頂點順序顛倒的矩形面積(難度:基礎)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex3_rect_area.c
 * 執行:/tmp/out
 */
#include <stdio.h>

struct point {
    int x;
    int y;
};

struct rect {
    struct point pt1;
    struct point pt2;
};

/* 小工具:min/max */
static int imin(int a, int b) { return (a < b) ? a : b; }
static int imax(int a, int b) { return (a > b) ? a : b; }

/*
 * 面積 = 寬 × 高。
 * 不假設 pt1 是左下、pt2 是右上——先用 min/max 正規化:
 * 寬 = |x2 - x1|,高 = |y2 - y1|,用 max-min 一次搞定。
 * 傳 const 指標:大結構不複製,且保證不修改。
 */
int rect_area(const struct rect *r)
{
    int width  = imax(r->pt1.x, r->pt2.x) - imin(r->pt1.x, r->pt2.x);
    int height = imax(r->pt1.y, r->pt2.y) - imin(r->pt1.y, r->pt2.y);
    return width * height;
}

int main(void)
{
    /* 標準順序:pt1 = 左下,pt2 = 右上 */
    struct rect normal = { { 0, 0 }, { 4, 3 } };

    /* 顛倒順序:pt1 = 右上,pt2 = 左下 */
    struct rect flipped = { { 4, 3 }, { 0, 0 } };

    /* 交錯順序:pt1 = 左上,pt2 = 右下 */
    struct rect crossed = { { 0, 3 }, { 4, 0 } };

    /* 退化矩形:兩點同一條線上 */
    struct rect line = { { 2, 5 }, { 9, 5 } };

    printf("正常順序   面積 = %d\n", rect_area(&normal));
    printf("完全顛倒   面積 = %d\n", rect_area(&flipped));
    printf("左上右下   面積 = %d\n", rect_area(&crossed));
    printf("退化成線段 面積 = %d\n", rect_area(&line));

    return 0;
}

程式碼講解

  • r->pt1.x:混合運算子的讀法——r 是指標所以第一層用 ->; pt1 是結構本體(不是指標)所以第二層用 .。 「有星號(指標)就用箭頭」的口訣在巢狀結構特別好用。
  • 四組測資的設計對應解題思路第 1 點的分析: 「左上右下」那組正是天真寫法會回 -12 的殺手案例,輸出證明正規化有效。
  • imin/imax 宣告成 static:檔案私有的小工具。 也可以用巨集 #define MAX(a,b) ...,但函式版沒有重複求值的陷阱(第 10 單元詳談)。
  • 呼叫端傳 &normal:對區域結構變數取址,完全合法——它活到 main 結束。

範例輸出

正常順序   面積 = 12
完全顛倒   面積 = 12
左上右下   面積 = 12
退化成線段 面積 = 0

練習 4:top_student —— 回傳最高分學生的指標(難度:進階)

題目

建立 struct student { char name[20]; int score; } 的陣列(至少 5 筆寫死資料), 寫函式找出最高分者並回傳其指標:

const struct student *top_student(const struct student a[], int n);

n <= 0 時回傳 NULL。測試包含兩人同分的情況(應回傳先出現者), 並用指標相減算出最高分者在陣列中的索引。

解題思路

  1. 為什麼回傳指標而不是複本? - 省複製(struct student 24 bytes,複製不貴但無必要); - 呼叫端拿到指標後可以直接讀原始資料、算索引(top - class)、 甚至修改那筆資料(若介面不加 const)。 回傳指標的安全前提:指向的資料在函式返回後仍然存在—— 這裡指向呼叫端的陣列,安全(對比:指向函式區域變數就是懸空指標)。
  2. 擂台法:best 先指向第一筆,走訪其餘元素,遇到更高分就把 best 換成 &a[i]。 比較條件用 >(嚴格大於):同分時不換人,自然保留「先出現者」。
  3. 防衛:n <= 0 時陣列裡沒有東西,回傳 NULL;呼叫端使用前檢查 (第 01 單元 safe_deref 的紀律又出現了)。
  4. const 的一致性:輸入是 const struct student a[], 回傳型別也得是 const struct student *——不能經由回傳值「偷渡」出可寫指標。

參考解答

/*
 * ex4_top_student.c
 * -----------------
 * 練習 4:結構陣列 —— 找出最高分的學生並回傳其指標(難度:進階)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex4_top_student.c
 * 執行:/tmp/out
 */
#include <stdio.h>

struct student {
    char name[20];
    int score;
};

/*
 * 回傳「指向最高分那筆資料的指標」而不是複本:
 *   - 不複製整個結構(省成本)
 *   - 呼叫端可經由指標直接讀(甚至改)原始資料
 * n <= 0 時回傳 NULL(防衛)。
 */
const struct student *top_student(const struct student a[], int n)
{
    if (n <= 0)
        return NULL;

    const struct student *best = &a[0];   /* 先假設第一筆最高 */

    for (int i = 1; i < n; i++)
        if (a[i].score > best->score)     /* 走訪擂台法 */
            best = &a[i];

    return best;
}

int main(void)
{
    struct student class[] = {
        { "Alice",   88 },
        { "Bob",     72 },
        { "Charlie", 95 },
        { "Diana",   95 },   /* 與 Charlie 同分:擂台法保留先出現者 */
        { "Eve",     61 },
    };
    int n = (int)(sizeof class / sizeof class[0]);

    printf("全班成績:\n");
    for (int i = 0; i < n; i++)
        printf("  %-8s %3d\n", class[i].name, class[i].score);

    const struct student *top = top_student(class, n);
    if (top != NULL) {
        printf("最高分:%s(%d 分)\n", top->name, top->score);
        /* 用指標相減可知是第幾筆 */
        printf("(陣列中的索引:%td)\n", top - class);
    }

    /* 邊界:空陣列 */
    printf("空陣列 -> %s\n",
           (top_student(class, 0) == NULL) ? "NULL(正確防衛)" : "錯誤");

    return 0;
}

程式碼講解

  • a[i].score > best->score:左邊 a[i] 是結構本體用 .; 右邊 best 是指標用 ->——同一行出現兩種存取運算子,正好對照。
  • best = &a[i];:換人只是換一個位址,不搬結構—— 和上一單元「排序字串只換指標」是同一種省力哲學。
  • top - class:兩個指向同一結構陣列的指標相減,得到相隔的元素數 (編譯器自動除以 sizeof(struct student)),印出用 %td。
  • 同分測試(Charlie/Diana 都是 95)驗證了 > 的「先到先贏」語意; 若想要「後出現者贏」才改用 >=——一個字元的差異要靠測試釘住。
  • 易錯處:把函式寫成回傳 struct student *(沒 const)卻收 const 陣列—— 編譯器會抓 const 流失。

範例輸出

全班成績:
  Alice     88
  Bob       72
  Charlie   95
  Diana     95
  Eve       61
最高分:Charlie(95 分)
(陣列中的索引:2)
空陣列 -> NULL(正確防衛)

練習 5:sort_by_score —— 結構陣列排序(難度:進階)

題目

寫函式 void sort_by_score(struct student a[], int n): 按分數由高到低排序結構陣列(交換單位是整個結構)。 測試資料至少 6 筆、包含同分者。

解題思路

  1. 和字串排序的差別:上一單元排字串換的是指標; 這裡的陣列直接存放結構本體,交換就得搬整個結構。 好消息:C 的結構支援整體指派 a[i] = a[best];—— 編譯器把所有成員(含 name 這個字元陣列!)一起複製, 不需要 strcpy 逐成員搬。
  2. 演算法:選擇排序——每輪從未排序區找分數最高者換到最前面。 由高到低只需把比較方向設為 a[j].score > a[best].score。
  3. 交換三行:臨時變數的型別是 struct student(不是指標):
struct student tmp = a[i];
a[i] = a[best];
a[best] = tmp;
  1. 同分的行為:選擇排序在這個寫法下同分者保持相對順序與否並無保證 (選擇排序本身不穩定);測試裡 Diana 與 Frank 同 80 分, 輸出顯示 Diana 在前(這次剛好維持原順序)。若業務上需要穩定排序, 要改用插入排序或在比較中加入第二鍵(如姓名)。

參考解答

/*
 * ex5_sort_by_score.c
 * -------------------
 * 練習 5:sort_by_score —— 按分數由高到低排序結構陣列(難度:進階)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex5_sort_by_score.c
 * 執行:/tmp/out
 */
#include <stdio.h>

struct student {
    char name[20];
    int score;
};

/*
 * 選擇排序,由高到低。
 * 交換的單位是「整個結構」——結構可以直接用 = 整體指派,
 * 編譯器會把所有成員(含 name 陣列)一起複製。
 */
void sort_by_score(struct student a[], int n)
{
    for (int i = 0; i < n - 1; i++) {
        int best = i;                        /* 找 [i, n) 中分數最高者 */
        for (int j = i + 1; j < n; j++)
            if (a[j].score > a[best].score)
                best = j;

        if (best != i) {                     /* 整個結構三行交換 */
            struct student tmp = a[i];
            a[i] = a[best];
            a[best] = tmp;
        }
    }
}

static void print_all(const char *title, const struct student a[], int n)
{
    printf("%s\n", title);
    for (int i = 0; i < n; i++)
        printf("  %d. %-8s %3d\n", i + 1, a[i].name, a[i].score);
}

int main(void)
{
    struct student class[] = {
        { "Alice",   88 },
        { "Bob",     72 },
        { "Charlie", 95 },
        { "Diana",   80 },
        { "Eve",     61 },
        { "Frank",   80 },   /* 與 Diana 同分 */
    };
    int n = (int)(sizeof class / sizeof class[0]);

    print_all("排序前:", class, n);
    sort_by_score(class, n);
    print_all("排序後(由高到低):", class, n);

    return 0;
}

程式碼講解

  • struct student tmp = a[i];:宣告即複製整個結構。 name 陣列是結構的一部分,跟著整包複製——這是「陣列不能指派、 但包在結構裡的陣列可以跟著結構指派」的有趣角落。
  • 降冪只差在比較方向;初學者常犯的錯是改成 < 之後忘了變數名 best 已不再是「最小」—— 命名和語意要一起改,否則三個月後讀不懂。
  • 交換前的 if (best != i) 是小優化:自己跟自己交換沒有意義。
  • 這裡是 O(n²) 的教學實作;真實程式排大陣列該用 qsort(標準庫)+ 自訂比較函式——那需要函式指標,屬於進階主題的預告。

範例輸出

排序前:
  1. Alice     88
  2. Bob       72
  3. Charlie   95
  4. Diana     80
  5. Eve       61
  6. Frank     80
排序後(由高到低):
  1. Charlie   95
  2. Alice     88
  3. Diana     80
  4. Frank     80
  5. Bob       72
  6. Eve       61

練習 6:padding 觀察 —— 成員順序影響結構大小(難度:挑戰)

題目

比較 struct { char a; int b; char c; } 與 struct { int b; char a; char c; } 的 sizeof, 用 offsetof(<stddef.h>)印出每個成員的實際位移,解釋兩者差異的原因, 並計算「一百萬筆資料的陣列」兩種佈局各占多少記憶體。

解題思路

  1. 背景:CPU 存取「對齊」的資料較快(4-byte 的 int 要放在 4 的倍數位址上), 編譯器會在成員間插入填補位元組(padding)來滿足對齊。
  2. 推演版本 A(char a; int b; char c;)在 64 位元 macOS 上的佈局: - a 放 offset 0(1 byte); - b 需要 4-byte 對齊 → 跳過 offset 1..3(3 bytes padding)→ 放 offset 4..7; - c 放 offset 8(1 byte); - 整個結構的大小必須是「最大對齊需求(4)」的倍數 (陣列中下一個元素的 b 才能對齊)→ 尾端再補 3 bytes → sizeof = 12。
  3. 推演版本 B(int b; char a; char c;): - b 放 offset 0..3(天然對齊,零浪費); - a 放 offset 4、c 放 offset 5(char 對齊需求 1,緊緊相鄰); - 尾端補 2 bytes 湊成 4 的倍數 → sizeof = 8。
   bad :  [a][-][-][-][B][B][B][B][c][-][-][-]   = 12 bytes(6 bytes 是 padding)
   good:  [B][B][B][B][a][c][-][-]               = 8 bytes(2 bytes 是 padding)
  1. 驗證工具:sizeof 看總大小、offsetof(型別, 成員) 看每個成員的實際位移—— 兩者都是編譯期常數,程式印出來對照推演。
  2. 實務結論:「成員由大到小排列」通常最省; 單筆差 4 bytes 不痛,乘上一百萬筆就是 4 MB——結構佈局是免費的效能優化。

參考解答

/*
 * ex6_padding.c
 * -------------
 * 練習 6:padding 觀察 —— 成員順序如何影響結構大小(難度:挑戰)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex6_padding.c
 * 執行:/tmp/out
 */
#include <stdio.h>
#include <stddef.h>    /* offsetof */

/* 版本 A:char / int / char 交錯——padding 最多 */
struct bad_layout {
    char a;     /* 1 byte,後面補 3 bytes 讓 b 對齊到 4 */
    int  b;     /* 4 bytes */
    char c;     /* 1 byte,後面補 3 bytes 讓整體大小是 4 的倍數 */
};

/* 版本 B:把大成員放前面——padding 最少 */
struct good_layout {
    int  b;     /* 4 bytes,天然對齊 */
    char a;     /* 1 byte */
    char c;     /* 1 byte,尾端只需補 2 bytes */
};

int main(void)
{
    printf("struct bad_layout  { char a; int b; char c; }\n");
    printf("  sizeof = %zu bytes(成員總和只有 6!)\n",
           sizeof(struct bad_layout));
    printf("  offsetof(a) = %zu\n", offsetof(struct bad_layout, a));
    printf("  offsetof(b) = %zu(a 之後跳過 3 bytes 的 padding)\n",
           offsetof(struct bad_layout, b));
    printf("  offsetof(c) = %zu(尾端還有 3 bytes padding)\n\n",
           offsetof(struct bad_layout, c));

    printf("struct good_layout { int b; char a; char c; }\n");
    printf("  sizeof = %zu bytes(省下 4 bytes)\n",
           sizeof(struct good_layout));
    printf("  offsetof(b) = %zu\n", offsetof(struct good_layout, b));
    printf("  offsetof(a) = %zu\n", offsetof(struct good_layout, a));
    printf("  offsetof(c) = %zu(a、c 緊緊相鄰,尾端補 2 bytes)\n\n",
           offsetof(struct good_layout, c));

    /* 佈局示意(B=b 的 byte,-=padding):
     *   bad :  a - - - B B B B c - - -   = 12 bytes
     *   good:  B B B B a c - -           = 8 bytes           */
    printf("記憶體佈局示意:\n");
    printf("  bad : [a][-][-][-][B][B][B][B][c][-][-][-] = 12\n");
    printf("  good: [B][B][B][B][a][c][-][-]             = 8\n\n");

    /* 陣列會放大差距:一百萬筆資料差 4 MB */
    printf("一百萬筆的陣列大小:bad = %zu bytes,good = %zu bytes\n",
           1000000 * sizeof(struct bad_layout),
           1000000 * sizeof(struct good_layout));

    return 0;
}

程式碼講解

  • offsetof(struct bad_layout, b) 回傳 4:直接證明 a 後面躺著 3 bytes 的 padding。 offsetof 是標準提供的巨集,比手動用指標運算猜位移可靠且可攜。
  • 「結構大小必須是最大成員對齊需求的倍數」的原因: struct bad_layout arr[2] 中,arr[1].b 也要對齊到 4—— 只有整個結構大小是 4 的倍數才能保證。
  • sizeof 的結果型別是 size_t,印出用 %zu。
  • 兩個結構的成員總和都是 6 bytes,佈局卻差出一倍—— 永遠用 sizeof,不要手加成員大小(做二進位 I/O 或 malloc 時尤其致命)。
  • 注意這些數字是「本平台(Apple clang / arm64)」的結果; 別的 ABI 可能不同,跨平台傳輸不能依賴結構佈局(第 11 單元會再強調)。

範例輸出

struct bad_layout  { char a; int b; char c; }
  sizeof = 12 bytes(成員總和只有 6!)
  offsetof(a) = 0
  offsetof(b) = 4(a 之後跳過 3 bytes 的 padding)
  offsetof(c) = 8(尾端還有 3 bytes padding)

struct good_layout { int b; char a; char c; }
  sizeof = 8 bytes(省下 4 bytes)
  offsetof(b) = 0
  offsetof(a) = 4
  offsetof(c) = 5(a、c 緊緊相鄰,尾端補 2 bytes)

記憶體佈局示意:
  bad : [a][-][-][-][B][B][B][B][c][-][-][-] = 12
  good: [B][B][B][B][a][c][-][-]             = 8

一百萬筆的陣列大小:bad = 12000000 bytes,good = 8000000 bytes

常見錯誤解法對照

錯誤寫法 症狀 正確做法
pc.center.x(pc 是指標) 編譯錯誤:指標要用 -> pc->center.x
傳值函式內改結構成員 呼叫端完全沒變 傳指標 f(struct point *p) + p->x
天真面積 (x2-x1)*(y2-y1) 頂點交錯時得到負面積 min/max 正規化寬與高
回傳函式內區域結構的指標 懸空指標,未定義行為 回傳結構值,或回傳指向呼叫端資料的指標
交換結構用 strcpy(a[i].name, ...) 逐成員搬 冗長且易漏成員 整體指派 a[i] = a[best];
手加成員大小當 sizeof 忽略 padding,buffer 算錯 一律用 sizeof/offsetof