學習目標
- 完整掌握
printf格式化:轉換符號、寬度、精度、旗標的組合。 - 理解
scanf的運作方式與著名陷阱:忘記&、緩衝區殘留的換行字元。 - 會用
sprintf/sscanf在「字串」與「格式化資料」之間互轉。 - 熟練
getchar讀到 EOF 的迴圈,理解標準輸入是「串流」。 - 建立「輸入永遠可能失敗,必須檢查回傳值」的防禦性心態。
對照 K&R 章節
- K&R §1.5 Character Input and Output
- K&R §7.1 Standard Input and Output
- K&R §7.2 Formatted Output—Printf
- K&R §7.3 Variable-length Argument Lists(僅概念)
- K&R §7.4 Formatted Input—Scanf
觀念講解
1. printf 格式字串的完整結構
%[旗標][寬度][.精度][長度修飾]轉換字元
轉換字元總表:
| 符號 | 意義 | 範例輸出 |
|---|---|---|
%d |
有號十進位整數 | 42、-7 |
%u |
無號十進位整數 | 42 |
%x / %X |
十六進位(小寫/大寫) | ff / FF |
%o |
八進位 | 377 |
%f |
定點浮點數(預設 6 位小數) | 3.141593 |
%e / %E |
科學記號 | 3.141593e+00 |
%g / %G |
自動選 %f 或 %e(去除尾零) |
3.14159 |
%c |
單一字元 | A |
%s |
字串(印到 '\0') |
hello |
%p |
指標位址 | 0x7ff... |
%% |
印出 % 本身 |
% |
長度修飾:%ld(long)、%lld(long long)、%zu(size_t)、%hd(short)。
旗標:
| 旗標 | 意義 |
|---|---|
- |
靠左對齊(預設靠右) |
+ |
正數也顯示 + 號 |
| (空格) | 正數留一格空白 |
0 |
用 0 補滿寬度 |
# |
替代形式:%#x 加 0x、%#o 加 0 |
寬度與精度:
printf("[%10d]\n", 42); /* [ 42] 最小寬度 10,靠右 */
printf("[%-10d]\n", 42); /* [42 ] 靠左 */
printf("[%010d]\n", 42); /* [0000000042] 補 0 */
printf("[%10.3f]\n", 3.14159);/* [ 3.142] 寬 10、小數 3 位 */
printf("[%.3s]\n", "hello"); /* [hel] 字串精度 = 最多印幾個字元 */
printf("[%*d]\n", 8, 42); /* [ 42] 寬度用引數動態指定 */
2. scanf 的運作方式
int age;
if (scanf("%d", &age) == 1) { /* 一定要傳位址(&)並檢查回傳值! */
printf("age = %d\n", age);
}
scanf需要變數的位址才能把讀到的值寫進去——&age的&就是取址。- 回傳值 = 成功轉換的項目數;輸入耗盡回傳
EOF。永遠檢查回傳值。 %d、%f等數值轉換會自動跳過前導空白(含換行);%c不會!- 讀字串用
%s(遇空白停止),變數本身是陣列時不加 &,且務必用寬度限制長度:scanf("%19s", name)。
3. scanf 兩大經典陷阱
陷阱 A:忘記 &
int x;
scanf("%d", x); /* 未定義行為:把 x 的「值」當位址亂寫 */
scanf("%d", &x); /* 正確 */
陷阱 B:緩衝區殘留的換行
輸入 42<Enter> 後,scanf("%d") 只拿走 42,換行字元還留在緩衝區。
接下來的 scanf("%c") 或 getchar() 會立刻讀到那個 '\n' 而不是等新輸入:
scanf("%d", &age);
scanf("%c", &initial); /* 讀到的是殘留的 '\n'! */
scanf(" %c", &initial); /* 修正:格式串前加空格,跳過空白類字元 */
清空殘留輸入的通用寫法:
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
; /* 丟棄到行尾 */
4. sprintf / sscanf:對字串做格式化
sprintf 把格式化結果寫進字元陣列(而非螢幕);sscanf 從字串解析(而非鍵盤):
char buf[64];
sprintf(buf, "%04d-%02d-%02d", 2026, 8, 1); /* buf = "2026-08-01" */
int y, m, d;
if (sscanf("2026-08-01", "%d-%d-%d", &y, &m, &d) == 3) {
/* y=2026, m=8, d=1 */
}
- 這對組合是「字串 ↔ 數值」轉換的瑞士刀:組訊息、拆記錄、驗證格式。
sprintf不檢查緩衝區大小;實務建議用snprintf(buf, sizeof buf, ...)(本單元範例採用)。sscanf的回傳值同樣是成功項目數——解析失敗的偵測全靠它。
5. getchar 迴圈與 EOF:輸入是串流
標準輸入是一條「字元串流」:可能來自鍵盤、檔案(<)、管線(|)。
程式不該假設「一定有人坐在鍵盤前」——讀到 EOF 就收工:
int c;
while ((c = getchar()) != EOF) {
/* 處理 c */
}
/* EOF:檔案讀完了 / 使用者按了 Ctrl-D / 管線來源結束 */
這個模式寫出來的程式天生就是「濾器(filter)」,能接上 UNIX 管線任意組合—— 這是 K&R 與 UNIX 哲學最深的交會點。
範例程式
| 檔案 | 內容 | 編譯與執行 |
|---|---|---|
01_printf_formats.c |
printf 格式化大全:所有轉換符號、寬度、精度、旗標的對照展示 | cc -std=c11 -Wall -Wextra -o 01_printf_formats 01_printf_formats.c && ./01_printf_formats |
02_scanf_pitfalls.c |
scanf 用法與陷阱:回傳值檢查、殘留換行實證與修正(無輸入時安全結束) | cc -std=c11 -Wall -Wextra -o 02_scanf_pitfalls 02_scanf_pitfalls.c,然後 printf "42 A\n3.5\n" \| ./02_scanf_pitfalls 或直接 ./02_scanf_pitfalls < /dev/null |
03_sprintf_sscanf.c |
字串格式化雙向轉換:組日期、拆 CSV、數值驗證、進位轉換 | cc -std=c11 -Wall -Wextra -o 03_sprintf_sscanf 03_sprintf_sscanf.c && ./03_sprintf_sscanf |
04_getchar_filter.c |
getchar 讀 EOF 濾器:行號器 + 統計,示範串流處理(無輸入也正常結束) | cc -std=c11 -Wall -Wextra -o 04_getchar_filter 04_getchar_filter.c && printf "one\ntwo\n" \| ./04_getchar_filter |
常見錯誤與陷阱
陷阱 1:格式符號與引數型別不符
long n = 123456789L;
printf("%d\n", n); /* 錯:%d 對 long 是未定義行為 */
printf("%ld\n", n); /* 對 */
double d = 3.14;
printf("%d\n", d); /* 錯得更離譜:%d 對 double */
clang 的 -Wformat(含在 -Wall)會抓出大多數這類錯誤——又一個開警告的理由。
陷阱 2:scanf 忘記 &(或對陣列多加 &)
int x; char name[20];
scanf("%d", x); /* 錯:缺 & */
scanf("%19s", &name); /* 錯:name 是陣列,本身就會退化成位址 */
scanf("%d %19s", &x, name); /* 對 */
陷阱 3:不檢查 scanf 回傳值
int x;
scanf("%d", &x); /* 使用者輸入 "abc" 時 x 沒被寫入! */
printf("%d\n", x); /* 印出垃圾值 */
修正:if (scanf("%d", &x) != 1) { /* 處理失敗 */ }。
更糟的是:轉換失敗時 "abc" 還留在緩衝區,下一次 scanf("%d") 依然失敗——
不清緩衝區就重試會變成無窮迴圈。
陷阱 4:%s 沒有寬度限制
char name[10];
scanf("%s", name); /* 輸入超過 9 字元就溢位! */
scanf("%9s", name); /* 修正:最多讀 9 個 + '\0' */
陷阱 5:混用 scanf 與 getchar 時忘了殘留換行
scanf("%d", &n);
int c = getchar(); /* 拿到 '\n' 而不是使用者的下一個輸入 */
修正:scanf 之後先清行(見觀念講解 3 的清空迴圈),或改用一致的逐行讀法。
練習題
- 基本題:用 printf 印出 255 的十進位、八進位(含
0前綴)、十六進位(含0x前綴)、以及%e與%g表示的 255.0。 - 對齊報表:用寬度與
-旗標印出三欄對齊的商品表(名稱靠左 12 格、數量靠右 5 格、單價靠右 8 格含 2 位小數)。 - 動態寬度:用
%*d寫一個函式print_right(int value, int width),並用它印出寬度 5、10、15 的同一個數字。 - 日期解析器:用
sscanf把"2026/08/01"、"2026-08-01"、"garbage"三種輸入試著解析成年月日,正確處理失敗案例(檢查回傳值)。 - 安全的整數輸入:寫一個「從 stdin 讀一個整數」的完整流程:scanf 失敗時清掉該行並提示重試,最多重試 3 次,EOF 時放棄。用
printf "abc\n42\n" | ./prog驗證。 - 挑戰題(迷你 CSV):給定寫死的字串陣列
{"Alice,30,1.65", "Bob,25,1.80", "broken line"},用sscanf逐筆拆出姓名(%[^,]或先用%19s簡化)、年齡、身高,跳過格式錯誤的資料列,最後印出平均年齡。
延伸閱讀
- K&R 2/e §7.1–7.4;附錄 B1.2(printf 完整規格表)
man 3 printf、man 3 scanf——最權威且永遠在手邊的參考- CERT C 安全編碼標準 FIO 章節:為什麼
gets被廢除、scanf("%s")為何危險 snprintf與 C11 Annex K 的_s系列——安全 I/O 的現代選項- 完成初階教程!中階單元將進入指標、結構、動態記憶體與檔案 I/O