printf 族函數是 C 語言格式化輸出的基石,但其可變參數機制、類型提升規則、格式字符串解析 暗藏諸多陷阱。本文從底層原理到安全實踐,構建完整認知體系。
| 函數 | 輸出目標 | 參數形式 | 頭文件 |
|---|---|---|---|
printf | stdout | fmt, ... | <stdio.h> |
fprintf | FILE* | fp, fmt, ... | |
sprintf | char* | buf, fmt, ... | |
snprintf | char* (限長) | buf, size, fmt, ... | |
vprintf | stdout | fmt, va_list | <stdarg.h> |
vfprintf | FILE* | fp, fmt, va_list | |
vsprintf | char* | buf, fmt, va_list | |
vsnprintf | char* (限長) | buf, size, fmt, va_list |
%[flags][width][.precision][length]specifier
| 組件 | 選項 | 示例 |
|---|---|---|
| flags | -左對齊 +符號 空格 #前綴 0填充 | %-10d %+d %#x |
| width | 最小寬度,* 從參數獲取 | %10d %*d |
| precision | .n 小數位/最大寬度,.* 從參數獲取 | %.2f %.5s |
| length | hh/h/l/ll/j/z/t/L | %hhd %ld %lld %zd %Lf |
| specifier | d/i/u/o/x/X/f/F/e/E/g/G/a/A/c/s/p/n/% | %d %s %p |
可變參數 `...` 部分發生默認參數提升:
float → doublechar/short (signed/unsigned) → int (或 unsigned int)後果: %f 總是接收 double;%c 接收 int;%hd 接收提升後的 int 再截斷。length 修飾符僅影響參數解讀方式,不改變提升規則。
參數從右向左壓棧 (cdecl),但 va_arg 從左向右順序讀取 (邏輯順序),由棧幀佈局保證一致性。
int sprintf(char *buf, const char *fmt, ...); int snprintf(char *buf, size_t size, const char *fmt, ...);
size 包含 \0 空間,永不溢出。必須檢查返回值 ≥ size 判斷截斷char buf[100] = "hello"; sprintf(buf, "%s world", buf); // UB! 來源與目標重疊 // 正確:snprintf(buf, sizeof(buf), "%s world", buf); // 仍 UB! // 必須用臨時緩衝區
void log_error(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap); // 轉發給 vfprintf
va_end(ap);
}
// C99 va_copy 支援多次遍歷
va_list ap2;
va_copy(ap2, ap);
// ... 使用 ap2 ...
va_end(ap2);
// 極度危險:用戶輸入作為格式字符串
printf(user_input); // %n 可寫入內存,%s 可洩漏棧數據
// 正確:
printf("%s", user_input); // 或 fputs(user_input, stdout)
%n:將已輸出字符數寫入對應 int* 參數 → 任意地址寫入-Wformat-security -Wformat=2 (GCC/Clang)| 方案 | 標準 | 優勢 | 示例 |
|---|---|---|---|
std::format | C++20 | 類型安全、Python 風格、無格式字符串漏洞 | std::format("{} {:.2f}", "pi", 3.14159) |
fmt::format (libfmt) | C++11+ | std::format 參考實現、性能極佳 | 同上 |
std::ostringstream | C++98 | 類型安全、可擴展、但較慢 | oss << "pi" << 3.14 |
snprintf | C99 | C 兼容、有長度保護 | snprintf(buf, sizeof(buf), "%.2f", 3.14) |
scanf/fscanf/sscanf/vscanf/...%s 無長度限制) 和格式字符串 風險fgets + sscanf / strto* / 手寫解析 / std::from_chars (C++17)printf 族函數通過格式字符串解析 + 棧參數提升 + va_list 掃描 實現多態輸出。核心避坑點:**禁用 sprintf、檢查 snprintf 返回值、拒絕用戶控制格式字符串、啟用編譯器格式檢查**。現代 C++ 專案請全面遷移至 std::format (C++20) 或 fmt::format,在編譯期消除格式化安全隱患,同時獲得更優的性能與可讀性。