C 語言通過 <stdarg.h> 四個宏實現可變參數:
va_list:參數列表指針類型(通常 char*)。va_start(ap, last_fixed):定位到第一個可變參數。va_arg(ap, type):取參數並推進指針。va_end(ap):清理(某平台需置 NULL)。
typedef char* va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) // 對齊到 int 倍數
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
#define va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define va_end(ap) (ap = (va_list)0)
關鍵假設:
__cdecl)。
void my_printf(const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
while (*fmt) {
if (*fmt != '%') { putchar(*fmt++); continue; }
switch (*++fmt) {
case 'd': printf("%d", va_arg(ap, int)); break;
case 's': printf("%s", va_arg(ap, char*)); break;
// ...
}
++fmt;
}
va_end(ap);
}
void Log(const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
vprintf(fmt, ap); // 標準庫提供 vprintf/vfprintf/vsprintf
va_end(ap);
}
// 被調用者接收 va_list
void SubFunc(const char* fmt, va_list ap) {
vprintf(fmt, ap);
}
// 調用者轉換
void MainFunc(const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
SubFunc(fmt, ap); // 直接傳遞
va_end(ap);
}
陷阱:va_arg(ap, int(*)()) 語法錯誤(函數指針類型無法直接放入宏)。
解決:typedef int (*FuncPtr)(); 然後 va_arg(ap, FuncPtr)。
規則:可變參數中 float 自動提升為 double,char/short 提升為 int。
正確用法:va_arg(ap, double) 取原 float,va_arg(ap, int) 取原 char/short。
ANSI C 要求 va_start 需要最後一個固定參數作為定位錨點,故 int f(...) 非法,必須 int f(int fixed, ...)。
| 錯誤 | 後果 | 修正 |
|---|---|---|
va_arg(ap, float) | 棧讀取錯位,後續參數全亂 | 用 double |
忘記 va_end | 某平台棧指針不恢復,崩潰 | 成對使用 |
多次遍歷同一 va_list | 指針已移至末尾,二次讀取垃圾 | 用 va_copy(dest, src) (C99) 複製副本 |
| 調用約定不匹配 | __stdcall 函數用 __cdecl 可變參數宏 | 統一約定,或用編譯器內建 __builtin_va_arg |
| 傳遞非 POD 類型 (C++) | 構造/析構失調,未定義行為 | 僅傳遞標量、指針、trivial 類型 |
template<typename... Args> void Log(Args&&... args),類型安全、零開銷、支援任意類型。fmt::print("{}: {}\n", "tag", 42),編譯期格式檢查,性能優於 printf。void Log(std::initializer_list<int>)。可變參數是 C 語言靈活性的基石,但類型不安全、依賴調用約定、易誤用。掌握 va_list 底層原理、對齊規則、類型提升規則、傳遞技巧,能寫出健壯的日誌、格式化、腳本綁定等基礎設施。C++ 新專案優先選擇可變參數模板或 std::format,從根源消除運行時風險。