本檔提供本單元練習題的完整詳解。強烈建議先自己動手做:每題先只看「題目」段落, 自己寫完、編譯、執行之後,再回來對照「解題思路」與「參考解答」。 所有解答的完整程式碼也放在 exercises/ 資料夾中,可直接編譯:

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

練習 1:自我介紹三行輸出(難度:基礎)

題目

修改 hello world,讓程式印出三行文字:你的名字、就讀學校(或公司)、一句座右銘。 每行結尾都要換行,且程式必須以 cc -std=c11 -Wall -Wextra 零警告編譯通過。

輸出範例(內容可換成你自己的):

Name  : Hsiao-Ming Wang
School: National Taiwan University
Motto : Talk is cheap. Show me the code.

解題思路

  1. 從已知出發:hello world 已經示範了「印出一行字」的完整骨架—— #include <stdio.h>、int main(void)、printf、return 0。本題只是把「一行」變成「三行」。
  2. 拆解需求:三行輸出 = 三次換行。有兩種等價做法: - 呼叫三次 printf,每次字串結尾放 \n; - 呼叫一次 printf,字串中間夾兩個 \n。 初學階段建議一行一個 printf,之後要改任何一行都不會影響其他行。
  3. 邊界檢查:最後一行也要 \n。少了它,shell 提示符號會黏在輸出後面—— 這是「輸出結尾永遠補換行」習慣的由來。
  4. 想讓冒號對齊的話,可以在字串裡手動補空格(如 "Name : ..."), 之後的單元會學到用 printf 寬度控制做得更漂亮。

參考解答

#include <stdio.h>

int main(void)
{
    /* 每個 printf 的字串結尾都要有 \n,輸出才會換行 */
    printf("Name  : Hsiao-Ming Wang\n");
    printf("School: National Taiwan University\n");
    printf("Motto : Talk is cheap. Show me the code.\n");

    return 0;   /* 回傳 0:告訴作業系統「正常結束」 */
}

(完整檔案:exercises/ex1_intro.c)

程式碼講解

  • #include <stdio.h>:沒有這行,編譯器不認識 printf 的原型。C99 之後這是硬性錯誤,不是警告。
  • 三個 printf 依序執行——C 程式在 main 內由上往下逐敘述執行。
  • 每個敘述結尾的分號 ; 不可省略;漏掉時編譯器報錯的行號常是「下一行」,要學會回頭看。
  • return 0;:向作業系統回報成功。忘了寫的話 C99 起 main 會隱含 return 0,但明確寫出是好習慣。

範例輸出

Name  : Hsiao-Ming Wang
School: National Taiwan University
Motto : Talk is cheap. Show me the code.

練習 2:跳脫序列(難度:基礎)

題目

只用一個 printf,印出下面這句包含雙引號與反斜線的句子(結尾換行):

She said: "C:\ is a path."

解題思路

  1. 找出「不能直接打」的字元: - 雙引號 "——直接打會被當成「字串常值的結尾」; - 反斜線 \——直接打會被當成「跳脫序列的開頭」。
  2. 查跳脫序列表:\" 代表雙引號本身、\\ 代表一個反斜線。
  3. 逐字翻譯:把目標句子從左到右掃一遍,遇到 " 換成 \"、遇到 \ 換成 \\, 其他字元照抄,最後補 \n。
  4. 驗證方法:編譯執行後用眼睛逐字比對;打錯常見症狀是編譯器報 missing terminating '"' character(字串被提早結束了)。

參考解答

#include <stdio.h>

int main(void)
{
    /* \" 代表雙引號本身;\\ 代表一個反斜線 */
    printf("She said: \"C:\\ is a path.\"\n");

    return 0;
}

(完整檔案:exercises/ex2_escape.c)

程式碼講解

  • 字串常值裡出現了三種跳脫:兩個 \"(句中的雙引號)、一個 \\(路徑裡的反斜線)、一個 \n(換行)。
  • 易錯處:C:\ is 若寫成 C:\(單一反斜線加空格),clang 會警告 unknown escape sequence '\ '—— 反斜線後面接什麼字元都會先被試著解讀成跳脫序列。
  • 記憶法:「想印出跳脫用的字元,就把它自己跳脫」——\ 印 \\、" 印 \"、%(printf 格式用)印 %%。

範例輸出

She said: "C:\ is a path."

練習 3:一個 printf 印多行(難度:基礎)

題目

字串常值不能直接跨行書寫(單元 README「陷阱 3」)。請用一個 printf 印出下面三行 Dennis Ritchie 的名言,並在程式中示範「相鄰字串常值自動串接」的寫法 (也就是把一個長字串拆成三段來寫,每段一行原始碼):

C is quirky, flawed, and an enormous success.
While accidents of history surely helped,
it evidently satisfied a need. -- Dennis Ritchie

解題思路

  1. 問題本質:想在原始碼裡「一行寫一段」,但輸出必須是一個字串。 C 提供的機制是:兩個(以上)相鄰的字串常值,編譯期會自動接成一個。
  2. 設計:把三行文字寫成三個字串常值,各自結尾放 \n, 然後直接排在一起(中間只有空白與換行,沒有逗號!),交給一個 printf。
  3. 易錯點預想: - 段與段之間若不小心打了逗號,printf 會把後面那段當成「對應格式的引數」, 行為完全不同(且 -Wall 會警告格式與引數數量不符)。 - 別忘了每段結尾的 \n——串接只是把字串接起來,不會自動幫你換行。

參考解答

#include <stdio.h>

int main(void)
{
    /* 相鄰的字串常值在編譯期會自動接成一個字串,
     * 所以下面三段其實是「一個」很長的字串、一次 printf 印完。
     * 每行的換行仍然靠 \n 表達,字串常值本身不能直接跨行。 */
    printf("C is quirky, flawed, and an enormous success.\n"
           "While accidents of history surely helped,\n"
           "it evidently satisfied a need. -- Dennis Ritchie\n");

    return 0;
}

(完整檔案:exercises/ex3_concat.c)

程式碼講解

  • 三個字串常值之間沒有任何運算子,前置處理後編譯器直接把它們拼成一個字串—— 這是 C 專門為「長字串排版」設計的語法。
  • 這個技巧在之後寫「長的格式字串」「使用說明文字」時非常常用。
  • 對照錯誤寫法:字串中間直接按 Enter 換行是編譯錯誤(missing terminating '"')。

範例輸出

C is quirky, flawed, and an enormous success.
While accidents of history surely helped,
it evidently satisfied a need. -- Dennis Ritchie

練習 4:結束狀態只有 8 位元(難度:進階)

題目

main 的 return 值會成為 shell 中 echo $? 看到的「結束狀態」,但它只有 8 位元(0~255)。

  1. 寫一支程式,對 0, 1, 42, 255, 256, 300, 511 這幾個候選 return 值, 印出「shell 實際上會看到的數字」(提示:取 % 256)。
  2. 親手實驗:把程式的 return 0; 暫時改成 return 42;,編譯執行後跑 echo $? 驗證; 再改成 return 256; 看看 shell 印出什麼。實驗完把程式改回 return 0;。

解題思路

  1. 背景知識:POSIX 系統上,父行程(shell)拿到的結束狀態只保留低 8 位元。 也就是說 shell 看到的是 return值 mod 256:256 變 0、300 變 44、511 變 255。
  2. 怎麼用程式展示:不必真的 return 那些值(一支程式只能 return 一次), 改成「印出一張對照表」——把候選值放進陣列,逐一印出 值 % 256。
  3. 驗證設計:程式印出的表是「預測」;步驟 2 的手動實驗是「實測」。 兩者一致,就證明了理解正確——這是很好的科學方法練習。
  4. 邊界情況:255 是最後一個「原樣呈現」的值;256 剛好繞回 0。 這也解釋了為什麼慣例上錯誤碼只用 1~255 的小數字。

參考解答

#include <stdio.h>

int main(void)
{
    /* 幾個代表性的 return 值,看看 shell 實際會看到什麼 */
    int values[] = { 0, 1, 42, 255, 256, 300, 511 };
    int n = (int)(sizeof values / sizeof values[0]);

    printf("return value -> what echo $? shows\n");
    printf("----------------------------------\n");
    for (int i = 0; i < n; i++) {
        /* 結束狀態只保留低 8 位元:等同於 % 256 */
        printf("return %3d;  ->  %d\n", values[i], values[i] % 256);
    }

    printf("\nconclusion: exit status is only 8 bits (0..255)\n");
    return 0;   /* 執行本程式後 echo $? 會印出 0 */
}

(完整檔案:exercises/ex4_exit_status.c)

程式碼講解

  • values[i] % 256:取 mod 256 等同「保留低 8 位元」,這就是作業系統對結束狀態做的事。
  • sizeof values / sizeof values[0]:陣列總位元組數除以單一元素位元組數 = 元素個數。 這是單元 07 會正式介紹的慣用法,這裡先見一面。
  • 手動實驗時的預期結果:return 42; → echo $? 印 42;return 256; → 印 0 (256 的低 8 位元全為 0)。
  • 陣列與 for 迴圈是後面單元的內容,此處只需看懂「逐一列印」的意圖即可。

範例輸出

return value -> what echo $? shows
----------------------------------
return   0;  ->  0
return   1;  ->  1
return  42;  ->  42
return 255;  ->  255
return 256;  ->  0
return 300;  ->  44
return 511;  ->  255

conclusion: exit status is only 8 bits (0..255)

執行後 echo $? 印出 0。


練習 5:%d 與 %s 初體驗(難度:進階)

題目

查閱 man 3 printf,了解 %d 與 %s 的意義,然後寫程式印出:

My favorite number is 7.
My favorite language is C, created in 1972.

要求:7 必須存放在 int 變數中並用 %d 帶入;第二行的 C 用 %s、1972 用 %d 帶入。

解題思路

  1. 查手冊:man 3 printf 中,conversion specifier 一節寫著: - d:int 引數以有號十進位印出(decimal); - s:引數是字串,逐字元印出直到結尾。
  2. printf 的運作模型:格式字串是「模板」,% 開頭的轉換符號是「洞」, 後面的引數依序填進洞裡。洞的個數、型別必須與引數一一對應。
  3. 步驟:宣告 int favorite = 7; → 模板寫 "My favorite number is %d.\n" → 把 favorite 當第二個引數傳給 printf。
  4. 常見錯誤預防:%d 給了字串、或 %s 給了整數,都是未定義行為。 幸好 clang 的 -Wall 內含 -Wformat,會直接警告——這是本教程堅持開警告的原因之一。

參考解答

#include <stdio.h>

int main(void)
{
    int favorite = 7;

    /* %d:印出一個 int(decimal,十進位) */
    printf("My favorite number is %d.\n", favorite);

    /* %s:印出一個字串;%d 與 %s 可以混用,依序對應後面的引數 */
    printf("My favorite language is %s, created in %d.\n", "C", 1972);

    return 0;
}

(完整檔案:exercises/ex5_format.c)

程式碼講解

  • 第一個 printf:一個洞(%d)對應一個引數(favorite)。
  • 第二個 printf:兩個洞依序對應——%s ← "C"、%d ← 1972。順序錯了型別就對不上。
  • 7. 結尾的句點是普通字元,直接寫在格式字串裡即可;只有 % 才有特殊意義 (想印 % 本身要寫 %%)。

範例輸出

My favorite number is 7.
My favorite language is C, created in 1972.

練習 6:前置處理器偵查(難度:挑戰)

題目

編譯流程的第一站是前置處理。寫一支程式,示範前置處理階段的兩種能力:

  1. 用 #define 定義符號常數 GREETING(一段問候字串)並印出。
  2. 印出這些「編譯器預先定義」的巨集,觀察它們的值: __FILE__(目前檔名)、__LINE__(目前行號)、__func__(目前函式名)、 __STDC_VERSION__(C 標準版本號,C11 為 201112L)。

解題思路

  1. #define 的本質:前置處理器的「文字替換」。#define GREETING "..." 之後, 程式中每個 GREETING 在編譯器看到之前就已被換成那段字串。
  2. 預定義巨集哪裡來:不需要 #include 任何檔案,編譯器天生就定義了它們。 __FILE__ 與 __LINE__ 展開成「這一行文字所在的位置」, 是日後寫除錯訊息、斷言(assert)的基礎建設。
  3. 型別偵查(本題的挑戰點):printf 格式必須配對正確—— - __FILE__ 展開成字串常值 → %s; - __LINE__ 展開成整數常數 → %d; - __func__ 是字串 → %s(注意它其實是 C99 的預定義識別字,不是巨集,所以在函式外不能用); - __STDC_VERSION__ 是 201112L——後綴 L 表示 long,必須用 %ld,用 %d 會被 -Wformat 警告。
  4. 驗證:__LINE__ 印出的數字應該正好是那行 printf 在檔案中的行號,打開編輯器對一下。

參考解答

#include <stdio.h>

/* 自訂的符號常數:前置處理器會把 GREETING 文字替換掉 */
#define GREETING "hello from the preprocessor"

int main(void)
{
    printf("%s\n\n", GREETING);

    /* __FILE__ / __LINE__:前置處理器展開時所在的檔名與行號 */
    printf("this line lives in file : %s\n", __FILE__);
    printf("this line's line number : %d\n", __LINE__);

    /* __func__:目前所在的函式名(C99 起) */
    printf("current function        : %s\n", __func__);

    /* __STDC_VERSION__:C 標準版本(C11 是 201112L),型別是 long */
    printf("C standard version      : %ld\n", __STDC_VERSION__);

    return 0;
}

(完整檔案:exercises/ex6_macros.c)

程式碼講解

  • #define GREETING "...":注意結尾沒有分號——它是前置處理指令不是 C 敘述, 多打分號的話分號會被一起替換進去,是經典錯誤。
  • __FILE__ 印出的是「編譯命令裡寫的路徑」:在 exercises/ 目錄下編譯就印 ex6_macros.c, 用完整路徑編譯就印完整路徑。
  • __LINE__ 的值是「它自己所在的那一行」,每次插入或刪除上方的程式行,值都會跟著變。
  • %ld 對 __STDC_VERSION__:後綴 L 的整數常數型別是 long, 格式不匹配雖然「看起來能動」,但屬於未定義行為,-Wformat 會抓出來。

範例輸出

hello from the preprocessor

this line lives in file : ex6_macros.c
this line's line number : 22
current function        : main
C standard version      : 201112

常見錯誤解法對照

錯誤寫法 症狀 正解
printf("hello") 少分號 編譯錯誤,行號指向下一行 每個敘述以 ; 結尾
void main() 非標準;作業系統拿不到正確結束狀態 int main(void)
字串裡直接打 " 或 \ missing terminating '"' 或 unknown escape \"、\\
字串常值直接跨行 編譯錯誤 \n 或相鄰字串常值串接
printf("%d\n", "hi") 型別錯配 -Wformat 警告;執行結果是垃圾 洞與引數型別一一對應
#define X 5; 多打分號 展開後多出分號,錯誤訊息離奇 #define 結尾不加分號