本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」,
寫完、編譯、執行後再對照「解題思路」與「參考解答」。
完整程式碼都在 exercises/ 資料夾,可直接編譯:
cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_printf_bases.c && /tmp/out
練習 1:printf 的各種數字面貌(難度:基礎)
題目
用 printf 印出:
- 整數 255 的十進位、八進位(含
0前綴)、十六進位(含0x前綴,大小寫各一); - 255.0 的
%e(科學記號)、%g(自動選擇)、%f(定點)三種表示; - 加碼:找兩個數字示範
%g什麼時候會切換成科學記號。
解題思路
- 前綴靠
#旗標:%o印377、%#o印0377;%x印ff、%#x印0xff。#的意義是「替代形式(alternative form)」,讓輸出自帶進位法標記。 - 三種浮點格式的分工:
-
%f:定點,預設 6 位小數(255.000000)——適合金額等固定小數場景; -%e:一律科學記號(2.550000e+02)——適合數量級差很大的科學數據; -%g:自動選短的且去尾零(255)——適合「給人看」的一般輸出。 %g的切換規則:十進位指數 < −4 或 ≥ 精度(預設 6)時改用科學記號。 找兩個例子夾出邊界:2.55 × 10⁸(指數 8 ≥ 6 → 科學記號)、 0.0000255(指數 −5 < −4 → 科學記號)。- 細節:格式字串裡要印出
%本身,寫%%。
參考解答
#include <stdio.h>
int main(void)
{
int n = 255;
double d = 255.0;
printf("decimal (%%d) : %d\n", n);
printf("octal (%%#o) : %#o\n", n); /* # 旗標:補上 0 前綴 */
printf("hex (%%#x) : %#x\n", n); /* # 旗標:補上 0x 前綴 */
printf("hex upper (%%#X) : %#X\n", n);
printf("scientific (%%e) : %e\n", d); /* 一律科學記號 */
printf("shortest (%%g) : %g\n", d); /* 自動選 %f 或 %e、去尾零 */
printf("fixed (%%f) : %f\n", d); /* 對照:定點、6 位小數 */
/* %g 什麼時候切換成科學記號?指數 < -4 或 >= 精度(預設 6)時 */
printf("\n%%g examples:\n");
printf(" %%g of 255000000.0 : %g\n", 255000000.0);
printf(" %%g of 0.0000255 : %g\n", 0.0000255);
return 0;
}
(完整檔案:exercises/ex1_printf_bases.c)
程式碼講解
%#X連前綴都變大寫(0XFF)——大小寫由轉換字元統一控制。%g印 255.0 時輸出就是255:小數點後全是零就整個省掉, 這是它適合「人類可讀輸出」的原因。- 同一個值、七種樣子——printf 的格式字串是「呈現層」,值本身從未改變 (與單元 02 練習 4 的觀念一脈相承)。
範例輸出
decimal (%d) : 255
octal (%#o) : 0377
hex (%#x) : 0xff
hex upper (%#X) : 0XFF
scientific (%e) : 2.550000e+02
shortest (%g) : 255
fixed (%f) : 255.000000
%g examples:
%g of 255000000.0 : 2.55e+08
%g of 0.0000255 : 2.55e-05
練習 2:對齊報表(難度:基礎)
題目
用寬度與 - 旗標印出三欄對齊的商品表:
名稱靠左 12 格、數量靠右 5 格、單價靠右 8 格(2 位小數)。
資料寫死至少四筆,其中一筆的名稱故意超過 12 個字元,觀察會發生什麼事。
解題思路
- 欄位規格翻成格式:
- 名稱靠左 12 格 →
%-12s(-旗標 = 靠左); - 數量靠右 5 格 →%5d(預設靠右); - 單價靠右 8 格、2 位小數 →%8.2f(寬度 8 含小數點與小數位)。 - 表頭要用同一組寬度:
printf("%-12s %5s %8s\n", "name", "qty", "price")—— 表頭也是字串,套同樣的欄寬才會跟資料對齊。分隔線同理。 - 關鍵觀察(本題的埋伏):
"dark chocolate"有 14 個字元 > 12。 printf 的寬度是最小寬度不是上限——欄位被撐開、該列右邊全部歪掉。 結論:寬度防「太短」不防「太長」。 - 想截斷怎麼辦:字串的精度才是上限——
%.12s最多印 12 個字元。 合起來%-12.12s就是「固定 12 格、超長截斷」的嚴格欄位。
參考解答
#include <stdio.h>
int main(void)
{
/* 平行陣列存放商品資料(結構 struct 是中階單元的主題) */
const char *names[] = { "apple", "bread", "milk", "dark chocolate" };
int qtys[] = { 12, 3, 25, 108 };
double prices[] = { 0.5, 32.0, 1.25, 99.9 };
int n = (int)(sizeof qtys / sizeof qtys[0]);
/* 表頭與資料用同一組寬度,欄位才會對齊:
* %-12s 靠左 12 格、%5d 靠右 5 格、%8.2f 靠右 8 格含 2 位小數 */
printf("%-12s %5s %8s\n", "name", "qty", "price");
printf("%-12s %5s %8s\n", "------------", "-----", "--------");
for (int i = 0; i < n; i++) {
printf("%-12s %5d %8.2f\n", names[i], qtys[i], prices[i]);
}
/* 觀察:名稱超過 12 格時(dark chocolate 有 14 字元),
* 寬度是「最小值」不是上限——欄位會被撐開而不是截斷 */
printf("\nnote: width is a MINIMUM; long names push the row wider.\n");
printf("to truncate, add precision: %%.12s -> [%.12s]\n",
"dark chocolate");
return 0;
}
(完整檔案:exercises/ex2_report.c)
程式碼講解
- 輸出中前三筆完美對齊,第四筆(dark chocolate)把整列撐歪—— 這一筆「壞資料」正是本題的教材:報表程式必須決定「撐開還是截斷」。
%8.2f的 8 是整個欄位寬度:32.00佔 5 字元、前面補 3 個空格。 想「小數點對齊」,讓所有列用同一個寬度即可。- 平行陣列(三個陣列用同一個索引 i 綁在一起)是還沒學
struct前的替代方案。
範例輸出
name qty price
------------ ----- --------
apple 12 0.50
bread 3 32.00
milk 25 1.25
dark chocolate 108 99.90
note: width is a MINIMUM; long names push the row wider.
to truncate, add precision: %.12s -> [dark chocola]
練習 3:動態寬度 %*d(難度:進階)
題目
用 %*d 寫函式 void print_right(int value, int width):
把 value 靠右印在 width 格寬的欄位裡(前後加 [] 標出邊界)。
用寬度 5、10、15 印同一個數字驗證。
加碼:試試負的寬度會發生什麼事;並示範一個實戰用法——
先掃描資料找出最寬的數字,再用它當全體的欄寬。
解題思路
%*d的機制:格式裡的*是佔位符, printf 會多吃一個 int 引數當寬度——printf("%*d", width, value)。 引數順序:先寬度、後數值,與*和d在格式裡的順序一致。- 為什麼需要它:寫死的
%10d在編譯期就固定了;%*d讓欄寬變成執行期的資料——正是「欄寬跟著資料走」的報表需求。 - 負寬度:標準規定負寬度等同「加上
-旗標的正寬度」——靠左對齊。 一個參數同時編碼「寬度 + 對齊方向」。 - 實戰示範的演算法:
- 第一趟:算每個數字的位數(
x /= 10數一次加一),記錄最大值; - 第二趟:全部用%*d配max_width印出——整欄剛好對齊、不多不少。 「兩趟走訪:先量再印」是排版程式的常用結構。
參考解答
#include <stdio.h>
/* 把 value 靠右印在 width 格寬的欄位裡(前後加 [] 標出欄位邊界) */
void print_right(int value, int width)
{
/* %*d 的星號會「吃掉」下一個引數當寬度——
* 寬度不再寫死在格式字串裡,而是執行期決定 */
printf("width %2d: [%*d]\n", width, width, value);
}
int main(void)
{
int value = 42;
print_right(value, 5);
print_right(value, 10);
print_right(value, 15);
/* 加碼:負寬度 = 靠左對齊(等同 - 旗標) */
printf("\nnegative width means left-align:\n");
printf("width -10: [%*d]\n", -10, value);
/* 動態寬度的實戰:欄寬跟著資料走 */
printf("\na practical use, aligning to the widest number:\n");
int data[] = { 7, 1024, 99, 123456 };
int n = (int)(sizeof data / sizeof data[0]);
int max_width = 1;
for (int i = 0; i < n; i++) { /* 算最寬的數字有幾位 */
int digits = 1;
for (int x = data[i]; x >= 10; x /= 10) {
digits++;
}
if (digits > max_width) {
max_width = digits;
}
}
for (int i = 0; i < n; i++) {
printf(" %*d\n", max_width, data[i]);
}
return 0;
}
(完整檔案:exercises/ex3_dynamic_width.c)
程式碼講解
printf("width %2d: [%*d]\n", width, width, value):width傳了兩次—— 第一次是印給人看的標籤,第二次餵給*。引數與洞的對應要逐一數清楚。- 數位數的內層迴圈:
x >= 10時不斷除 10——7 是 1 位、123456 是 6 位。 (用%d的輸出長度反推位數也可以,但直接算不依賴字串。) - 精度同樣可以動態化:
%.*f讓小數位數也變成引數——舉一反三。
範例輸出
width 5: [ 42]
width 10: [ 42]
width 15: [ 42]
negative width means left-align:
width -10: [42 ]
a practical use, aligning to the widest number:
7
1024
99
123456
練習 4:日期解析器(難度:進階)
題目
寫函式 void parse_date(const char *input),用 sscanf 解析日期字串,
同時支援 "2026/08/01"(斜線)與 "2026-08-01"(連字號)兩種格式。
測資包含:兩種正常格式、"garbage"、格式對但值不合理的 "2026-13-40"、
少一段的 "2026/08"。全部靠回傳值判斷成敗,失敗的要明確回報。
解題思路
- sscanf 是「從字串讀」的 scanf:格式字串裡的普通字元(
/、-) 要求輸入逐字匹配——這正是拿來區分兩種日期格式的工具。 - 回傳值是唯一的成敗依據:
sscanf(input, "%d/%d/%d", &y, &m, &d)回傳成功轉換的項目數。三個%d全中才是 3;"2026/08"只中 2、"garbage"中 0——用== 3判斷,不能只判> 0。 - 兩種格式的嘗試順序:先試斜線、失敗再試連字號,
用
||短路串接:第一個成功就不試第二個。 (對"2026-08-01"試斜線格式時,%d會把2026讀走、 遇到-與格式裡的/不符而停,回傳 1 ≠ 3 → 正確落到第二個格式。) - 解析成功 ≠ 值合理:
"2026-13-40"三個數字都讀得到(回傳 3), 但 13 月 40 日不存在——語法檢查之後還要語意檢查(月 1~12、日 1~31)。 這個「兩層驗證」的意識比本題本身更重要。 - 測資設計:每條路徑一筆——兩種成功、格式錯、值錯、段數不足。
參考解答
#include <stdio.h>
/* 嘗試解析一個日期字串:支援 YYYY/MM/DD 與 YYYY-MM-DD 兩種格式。
* 成功印出解析結果、失敗印出錯誤訊息。 */
void parse_date(const char *input)
{
int y, m, d;
/* 先試斜線格式;sscanf 回傳「成功轉換的項目數」,== 3 才是全額成功 */
if (sscanf(input, "%d/%d/%d", &y, &m, &d) == 3 ||
sscanf(input, "%d-%d-%d", &y, &m, &d) == 3) {
/* 解析成功不代表值合理:再做範圍檢查 */
if (m >= 1 && m <= 12 && d >= 1 && d <= 31) {
printf("[%s] -> year=%d month=%d day=%d\n", input, y, m, d);
} else {
printf("[%s] -> parsed but out of range (m=%d d=%d)\n",
input, m, d);
}
} else {
printf("[%s] -> not a date, skipped\n", input);
}
}
int main(void)
{
const char *inputs[] = {
"2026/08/01", /* 斜線格式 */
"2026-08-01", /* 連字號格式 */
"garbage", /* 完全不是日期 */
"2026-13-40", /* 格式對但值不合理 */
"2026/08", /* 少一段:sscanf 只回 2 */
};
int n = (int)(sizeof inputs / sizeof inputs[0]);
for (int i = 0; i < n; i++) {
parse_date(inputs[i]);
}
return 0;
}
(完整檔案:exercises/ex4_date_parser.c)
程式碼講解
||的短路特性讓「格式嘗試鏈」乾淨俐落:成功即止、失敗續試。 要支援第三種格式(如"2026.08.01")就再串一個。- 注意一個微妙點:第一個 sscanf 失敗時
y可能已被寫入(部分成功)—— 所以絕不能在確認回傳值前使用任何變數。 %d對"08"讀出 8:十進位轉換不會把0開頭當八進位(那是%i的行為), 日期解析用%d才安全。
範例輸出
[2026/08/01] -> year=2026 month=8 day=1
[2026-08-01] -> year=2026 month=8 day=1
[garbage] -> not a date, skipped
[2026-13-40] -> parsed but out of range (m=13 d=40)
[2026/08] -> not a date, skipped
練習 5:安全的整數輸入(難度:挑戰)
題目
寫一個「從 stdin 讀一個整數」的完整流程:
scanf("%d")失敗時,清掉該行並提示重試,最多 3 次;- 讀到 EOF 立刻放棄(不再空等);
- 無輸入(
< /dev/null)也要能正常結束、exit 0。
用三種方式驗證:
printf "abc\n42\n" | ./prog(先錯後對)、./prog < /dev/null(立刻 EOF)、
printf "x\ny\nz\n" | ./prog(三次全錯)。
解題思路
- scanf 的三種回傳值就是三條路:
-
1:成功——設旗標、跳出; -EOF:輸入耗盡——再重試也不會有新輸入,直接放棄; -0:這行開頭不是數字——進入「清理重試」流程。 - 為什麼必須清行(本題的靈魂):
%d轉換失敗時, 壞資料"abc"原封不動留在緩衝區。不清掉就重試, 下一輪 scanf 讀到的還是同一坨——瞬間吃完三次機會(實際上是無窮迴圈的成因)。 清行的標準寫法:while ((ch = getchar()) != '\n' && ch != EOF) ;。 - 清行也可能撞到 EOF:輸入的最後一行沒有換行就結束時, 清行迴圈以 EOF 收場——這也要當成「放棄」處理,否則下一輪 scanf 空轉。
- 控制結構:
for跑三次嘗試 +got_value旗標記錄成敗, 迴圈後統一收尾(成功印值、失敗印訊息)。成敗兩條路都要有明確輸出。 - exit code 的選擇:示範程式選擇「怎樣都 return 0」;
實務工具則常在讀不到時
return 1讓 shell 腳本能判斷——註解裡說明即可。
參考解答
#include <stdio.h>
#define MAX_RETRIES 3
int main(void)
{
int value = 0;
int got_value = 0; /* 旗標:是否成功讀到整數 */
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
printf("enter an integer (attempt %d of %d): ", attempt, MAX_RETRIES);
int rc = scanf("%d", &value); /* 回傳成功項目數,EOF 表示輸入耗盡 */
if (rc == 1) {
got_value = 1; /* 成功:跳出重試迴圈 */
break;
}
if (rc == EOF) {
printf("\nend of input reached, giving up.\n");
break; /* EOF:再重試也不會有輸入了 */
}
/* rc == 0:這一行的開頭不是數字。
* 壞資料還留在緩衝區,必須清掉整行,否則下一輪 scanf
* 又讀到同一坨垃圾——無窮迴圈的經典成因 */
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
; /* 丟棄到行尾 */
}
printf("that was not an integer, please try again.\n");
if (ch == EOF) { /* 清行清到 EOF:同樣放棄 */
printf("end of input reached, giving up.\n");
break;
}
}
if (got_value) {
printf("you entered: %d\n", value);
} else {
printf("no valid integer was read.\n");
}
return 0; /* 不論讀到與否都正常結束(示範程式的選擇) */
}
(完整檔案:exercises/ex5_safe_input.c)
程式碼講解
- 三種測試情境覆蓋三條路:
abc\n42\n走「失敗 → 清行 → 成功」;/dev/null走「第一次就 EOF」;x\ny\nz\n走「三次全失敗、機會用完」。全部 exit 0。 - 清行迴圈的
ch必須是int(要容納 EOF)——單元 08 陷阱 1 在這裡真實上場。 - 這一小段「檢查回傳值 + 清緩衝 + 上限重試 + EOF 放棄」就是 互動式輸入的完整防禦套路,值得整段背下來。
範例輸出
printf "abc\n42\n" | ./prog:
enter an integer (attempt 1 of 3): that was not an integer, please try again.
enter an integer (attempt 2 of 3): you entered: 42
./prog < /dev/null:
enter an integer (attempt 1 of 3):
end of input reached, giving up.
no valid integer was read.
printf "x\ny\nz\n" | ./prog(三次全錯):
enter an integer (attempt 1 of 3): that was not an integer, please try again.
enter an integer (attempt 2 of 3): that was not an integer, please try again.
enter an integer (attempt 3 of 3): that was not an integer, please try again.
no valid integer was read.
練習 6:迷你 CSV 解析(難度:挑戰)
題目
給定寫死的字串陣列(每筆是「姓名,年齡,身高」的 CSV 列):
{ "Alice,30,1.65", "Bob,25,1.80", "broken line", "Carol,41,1.72", "Dave,not-a-number,2" }
用 sscanf 逐筆拆出姓名(提示:%[^,])、年齡(int)、身高(double),
跳過格式錯誤的資料列,最後印出有效筆數與平均年齡。
解題思路
- 姓名欄的難題:
%s遇空白才停,但 CSV 的分隔符是逗號——%s會把"Alice,30,1.65"整串吞掉。 解法是掃描集(scanset)%[^,]:「讀取不是逗號的字元,直到遇到逗號」。 - 永遠加寬度限制:
%19[^,]——姓名緩衝區char name[20], 最多讀 19 個字元、留一格給'\0'。沒有寬度限制的%[^,]與%s同罪:緩衝區溢位。 - 完整格式:
"%19[^,],%d,%lf"—— 掃描集吃姓名、字面逗號要求分隔符就位、%d吃年齡、%lf吃身高 (scanf 家族讀 double 要用%lf,printf 用%f——經典不對稱)。 - 成敗判準:回傳值
== 3才算整列成功。"broken line":%19[^,]能吃下整行,但後面沒有逗號 → 回傳 1;"Dave,not-a-number,2":姓名成功、%d對上not-a-number失敗 → 回傳 1。 兩者都被== 3擋下——一個判準攔住所有壞法。 - 統計:
total_age與valid只在成功分支累加; 平均前檢查valid > 0(除以零防線),輸出(double)total_age / valid。
參考解答
#include <stdio.h>
int main(void)
{
/* 每個元素是一列 CSV 資料:姓名,年齡,身高 */
const char *lines[] = {
"Alice,30,1.65",
"Bob,25,1.80",
"broken line", /* 沒有逗號:解析必失敗 */
"Carol,41,1.72",
"Dave,not-a-number,2", /* 年齡欄壞掉:只成功 1 項 */
};
int n = (int)(sizeof lines / sizeof lines[0]);
int total_age = 0;
int valid = 0;
printf("%-8s %4s %7s\n", "name", "age", "height");
printf("%-8s %4s %7s\n", "--------", "----", "-------");
for (int i = 0; i < n; i++) {
char name[20];
int age;
double height;
/* %19[^,]:讀「逗號以外的字元」至多 19 個(留 1 格給 '\0'),
* 之後的 ,%d,%lf 依序吃掉年齡與身高。
* 回傳值必須是 3 才代表整列解析成功。 */
if (sscanf(lines[i], "%19[^,],%d,%lf", name, &age, &height) == 3) {
printf("%-8s %4d %7.2f\n", name, age, height);
total_age += age;
valid++;
} else {
printf("skipped bad line: \"%s\"\n", lines[i]);
}
}
if (valid > 0) {
printf("\nvalid rows : %d of %d\n", valid, n);
printf("average age: %.1f\n", (double)total_age / valid);
} else {
printf("\nno valid rows.\n");
}
return 0;
}
(完整檔案:exercises/ex6_mini_csv.c)
程式碼講解
sscanf(..., name, &age, &height):name是陣列不加 &(自動退化成位址),age、height是普通變數必須加 &——README 陷阱 2 的正反兩面同框。- 平均年齡 (30 + 25 + 41) / 3 = 32.0,輸出吻合;
兩筆壞資料各以自己的方式失敗,都被同一個
== 3攔截。 - 侷限說明:
%[^,]無法處理「空欄位」(連續兩個逗號會失敗)與含逗號的引號欄位—— 工業級 CSV 要逐字元寫小解析器;本題的重點是 sscanf 回傳值的防禦性使用。
範例輸出
name age height
-------- ---- -------
Alice 30 1.65
Bob 25 1.80
skipped bad line: "broken line"
Carol 41 1.72
skipped bad line: "Dave,not-a-number,2"
valid rows : 3 of 5
average age: 32.0
常見錯誤解法對照
| 錯誤寫法 | 症狀 | 正解 |
|---|---|---|
printf("%d", 3.14) 型別錯配 |
垃圾輸出;-Wformat 警告 |
格式與型別一一對應 |
scanf("%d", x) 忘記 & |
未定義行為 | &x(陣列除外) |
| 不檢查 scanf/sscanf 回傳值 | 用到沒被寫入的變數 | if (rc == 期望項目數) |
| scanf 失敗後直接重試 | 壞資料留在緩衝區,無窮迴圈 | 先清行再重試 |
scanf("%s", name) 無寬度 |
緩衝區溢位 | %19s / %19[^,] |
scanf 讀 double 用 %f |
讀進 float 大小的記憶體,值全毀 | scanf 家族用 %lf |
| 以為寬度會截斷字串 | 長資料撐歪整列 | 寬度是最小值;截斷用精度 %.12s |