本檔提供本單元練習題的完整詳解。強烈建議先自己動手做:每題先只看「題目」段落,
自己寫完、編譯、執行之後,再回來對照「解題思路」與「參考解答」。
所有解答的完整程式碼也放在 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.
解題思路
- 從已知出發:hello world 已經示範了「印出一行字」的完整骨架——
#include <stdio.h>、int main(void)、printf、return 0。本題只是把「一行」變成「三行」。 - 拆解需求:三行輸出 = 三次換行。有兩種等價做法:
- 呼叫三次
printf,每次字串結尾放\n; - 呼叫一次printf,字串中間夾兩個\n。 初學階段建議一行一個printf,之後要改任何一行都不會影響其他行。 - 邊界檢查:最後一行也要
\n。少了它,shell 提示符號會黏在輸出後面—— 這是「輸出結尾永遠補換行」習慣的由來。 - 想讓冒號對齊的話,可以在字串裡手動補空格(如
"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."
解題思路
- 找出「不能直接打」的字元:
- 雙引號
"——直接打會被當成「字串常值的結尾」; - 反斜線\——直接打會被當成「跳脫序列的開頭」。 - 查跳脫序列表:
\"代表雙引號本身、\\代表一個反斜線。 - 逐字翻譯:把目標句子從左到右掃一遍,遇到
"換成\"、遇到\換成\\, 其他字元照抄,最後補\n。 - 驗證方法:編譯執行後用眼睛逐字比對;打錯常見症狀是編譯器報
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
解題思路
- 問題本質:想在原始碼裡「一行寫一段」,但輸出必須是一個字串。 C 提供的機制是:兩個(以上)相鄰的字串常值,編譯期會自動接成一個。
- 設計:把三行文字寫成三個字串常值,各自結尾放
\n, 然後直接排在一起(中間只有空白與換行,沒有逗號!),交給一個printf。 - 易錯點預想:
- 段與段之間若不小心打了逗號,
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)。
- 寫一支程式,對
0, 1, 42, 255, 256, 300, 511這幾個候選 return 值, 印出「shell 實際上會看到的數字」(提示:取% 256)。 - 親手實驗:把程式的
return 0;暫時改成return 42;,編譯執行後跑echo $?驗證; 再改成return 256;看看 shell 印出什麼。實驗完把程式改回return 0;。
解題思路
- 背景知識:POSIX 系統上,父行程(shell)拿到的結束狀態只保留低 8 位元。
也就是說 shell 看到的是
return值 mod 256:256 變 0、300 變 44、511 變 255。 - 怎麼用程式展示:不必真的 return 那些值(一支程式只能 return 一次),
改成「印出一張對照表」——把候選值放進陣列,逐一印出
值 % 256。 - 驗證設計:程式印出的表是「預測」;步驟 2 的手動實驗是「實測」。 兩者一致,就證明了理解正確——這是很好的科學方法練習。
- 邊界情況: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 帶入。
解題思路
- 查手冊:
man 3 printf中,conversion specifier 一節寫著: -d:int 引數以有號十進位印出(decimal); -s:引數是字串,逐字元印出直到結尾。 - printf 的運作模型:格式字串是「模板」,
%開頭的轉換符號是「洞」, 後面的引數依序填進洞裡。洞的個數、型別必須與引數一一對應。 - 步驟:宣告
int favorite = 7;→ 模板寫"My favorite number is %d.\n"→ 把favorite當第二個引數傳給printf。 - 常見錯誤預防:
%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:前置處理器偵查(難度:挑戰)
題目
編譯流程的第一站是前置處理。寫一支程式,示範前置處理階段的兩種能力:
- 用
#define定義符號常數GREETING(一段問候字串)並印出。 - 印出這些「編譯器預先定義」的巨集,觀察它們的值:
__FILE__(目前檔名)、__LINE__(目前行號)、__func__(目前函式名)、__STDC_VERSION__(C 標準版本號,C11 為201112L)。
解題思路
#define的本質:前置處理器的「文字替換」。#define GREETING "..."之後, 程式中每個GREETING在編譯器看到之前就已被換成那段字串。- 預定義巨集哪裡來:不需要
#include任何檔案,編譯器天生就定義了它們。__FILE__與__LINE__展開成「這一行文字所在的位置」, 是日後寫除錯訊息、斷言(assert)的基礎建設。 - 型別偵查(本題的挑戰點):printf 格式必須配對正確——
-
__FILE__展開成字串常值 →%s; -__LINE__展開成整數常數 →%d; -__func__是字串 →%s(注意它其實是 C99 的預定義識別字,不是巨集,所以在函式外不能用); -__STDC_VERSION__是201112L——後綴 L 表示 long,必須用%ld,用%d會被-Wformat警告。 - 驗證:
__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 結尾不加分號 |