研讀《The C Programming Language》時,隨手寫下幾行 `scanf` 測試代碼,卻踩中隱藏已久的「逗號分隔輸入」深坑。本文完整復現問題現場、GDB 調試還原真相、原理深度剖析,並提供兩種工業級解決方案。
#include <stdio.h>
int main() {
int a, b;
char mon[20];
int count;
count = scanf("%d,%s,%d", &a, mon, &b);
printf("%d,%s,%d\n", a, mon, b);
printf("%d\n", count);
return 0;
}
/* 輸入 */ 12,fefe,45
/* 輸出 */ 12,fefe,45,10359588 ← b 顯示亂碼 (未初始化殘留值)
/* 返回值 */ 2 ← 預期 3,實際只讀了 2 項!
(gdb) p a
$1 = 12
(gdb) p mon
$2 = "fe,45\000\377\277\245\324\025\000\060\340\021\000K\205\004\b"
/* mon 實際內容: "fe,45" + 結束符 + 棧上殘留垃圾數據 */
鐵證如山: `fefe,45` 作為**整體字符串**被 `%s` 吞下,`b` 根本未被賦值,`scanf` 提前匹配失敗返回 2。
輸入緩衝區初始: "12,fefe,45\n"
掃描過程:
1. "%d" → 讀 '1','2',遇 ',' 停止 → a=100 緩衝區剩 ",fefe,45\n"
2. ',' → 匹配 ',' 成功消耗 緩衝區剩 "fefe,45\n"
3. "%s" → 跳過空白(無) → 讀 'f','e','f','e',',' ,'4','5' 遇 '\n' 停止 → mon="fefe,45" 緩衝區剩 "\n"
4. ',' → 期望匹配 ',',實際是 '\n' → **匹配失敗!掃描終止**
5. "%d" → 根本沒機會執行
返回值 = 成功匹配項數 = 2
核心教訓: `%s` **不認逗號為分隔符**,它只認空白字符。逗號在格式串中是字面匹配要求,但 `%s` 已經把後面的逗號「吃」進字符串了,導致後續字面逗號匹配失敗。
scanf("%d,%[^,],%d", &a, mon, &b); // 核心:%[^,] 讀取「非逗號」字符集
/* 掃描集語法詳解:
%[chars] → 只讀取 chars 集合內的字符,遇非集合字符停止
%[^chars] → 讀取「非 chars 集合」的字符,遇集合內字符停止
自動補 '\0' 結束符
*/
// 測試:輸入 "12,fefe,45" → a=12, mon="fefe", b=45, count=3 ✅
優勢: 語義精確、無需修改輸入格式、標準 C 支持、可擴展任意分隔符集合 (如 `%[^;,]` 同時支援分號逗號)。
scanf("%d,%s ,%d", &a, mon, &b); // 注意 %s 後有一個空格
/* 輸入必須配合: "12,fefe ,45" (fefe 後手動加空格) */
缺點: 強制用戶輸入時在字符串後加空格,體驗差;多個連續空白/換行均被跳過,可能掩蓋輸入錯誤。**不推薦生產環境使用。**
// 1. 只讀數字
scanf("%[0-9]", buf); // 輸入 "123abc" → buf="123"
// 2. 讀取一行 (含空格,遇換行停止)
scanf("%[^\n]", line); // %[^\n] 讀取非換行字符
// 3. 讀取雙引號內容
scanf("\"%[^\"]\"", str); // 輸入 "\"hello world\"" → str="hello world"
// 4. 解析 CSV 行 (簡易版)
char field1[32], field2[32], field3[32];
scanf("%[^,],%[^,],%[^\n]", field1, field2, field3);
這個坑看似微小,實則觸及 C 語言輸入系統的核心機制:格式掃描是基於字符流的貪婪匹配,而非語義解析。理解緩衝區流轉、分隔符語義、掃描集威力,是寫出健壯 C 輸入代碼的必修課。