學習目標

  • 完整掌握 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 的清空迴圈),或改用一致的逐行讀法。

練習題

  1. 基本題:用 printf 印出 255 的十進位、八進位(含 0 前綴)、十六進位(含 0x 前綴)、以及 %e 與 %g 表示的 255.0。
  2. 對齊報表:用寬度與 - 旗標印出三欄對齊的商品表(名稱靠左 12 格、數量靠右 5 格、單價靠右 8 格含 2 位小數)。
  3. 動態寬度:用 %*d 寫一個函式 print_right(int value, int width),並用它印出寬度 5、10、15 的同一個數字。
  4. 日期解析器:用 sscanf 把 "2026/08/01"、"2026-08-01"、"garbage" 三種輸入試著解析成年月日,正確處理失敗案例(檢查回傳值)。
  5. 安全的整數輸入:寫一個「從 stdin 讀一個整數」的完整流程:scanf 失敗時清掉該行並提示重試,最多重試 3 次,EOF 時放棄。用 printf "abc\n42\n" | ./prog 驗證。
  6. 挑戰題(迷你 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