C 語言的可變參數函數(Variadic Functions)是系統編程的核心技能,printf 系列即是典型代表。本文從底層棧幀布局、stdarg.h 宏機制、實戰陷阱三個維度全景剖析。
函數參數個數、類型在編譯期不固定,由調用者決定。語法標誌為 ...:
int printf(const char *format, ...);
C/C++ 參數壓棧順序為從右向左(右most 先壓),因此第一個固定參數 format 總位於棧頂附近,易於定位。以 printf("%d,%d", a, b) 為例:
/* 彙編視角 */ push b ; 最後壓入,棧頂偏移最小 push a push format ; 最先壓入,但位於高地址 call printf
關鍵洞見: format 字符串解析出參數個數與類型 → 計算棧偏移 → 逐個讀取參數。
| 宏 | 功能 | 典型實現邏輯 |
|---|---|---|
va_list | 參數指針類型 | typedef char *va_list; |
va_start(ap, last) | 初始化指向第一個可變參數 | ap = (va_list)&last + _INTSIZEOF(last) |
va_arg(ap, type) | 取值並移動指針 | *(type*)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) |
va_end(ap) | 清理指針 | ap = (va_list)0 |
_INTSIZEOF 宏: 確保參數按 sizeof(int) 對齊(歷史遺留,早期 CPU 要求)。
#include <stdarg.h>
#include <iostream>
using namespace std;
int sum(int n, ...) {
va_list vap;
va_start(vap, n);
int s = 0;
for (int i = 0; i < n; ++i)
s += va_arg(vap, int); // 假設全為 int
va_end(vap);
return s;
}
int main() {
cout << sum(3, 45, 89, 72) << endl; // 206
return 0;
}
format 中的 % 佔位符計數%d/%f/%s/%p... 動態決定 va_arg 的類型l/h/L、寬度、精度等格式化細節float → double、char/short → intva_arg(ap, float) → 正確:va_arg(ap, double)int f(...) 非法,ANSI C 要求 int f(int fixed, ...),va_start 需要最後一個固定參數作為定位錨點。
編譯器不檢查 ... 部分,格式字符串與實參不匹配導致 UB。printf("%d", "str") 編譯通過但運行崩潰。
va_list 可能不可重用某些架構下 va_list 為結構體而非指針,傳遞給其他函數需用 va_copy (C99)。
va_start 必須配對 va_end特別是循環/多次調用場景,否則棧狀態損壞。
C++ 中傳遞 std::string 等類類型到 ... 行為未定義,需用 va_arg(ap, const char*) 配合 .c_str()。
void myprintf(const char *fmt, ...) {
va_list ap; va_start(ap, fmt);
while (*fmt) {
if (*fmt != '%') { putchar(*fmt++); continue; }
switch (*++fmt) {
case 'd': { int n = va_arg(ap, int); /* itoa輸出 */ break; }
case 'f': { double f = va_arg(ap, double); /* 處理小數 */ break; }
case 's': { char *s = va_arg(ap, char*); fputs(s, stdout); break; }
case 'c': putchar(va_arg(ap, int)); break;
case '%': putchar('%'); break;
default: fputs("format invalid!", stdout);
}
fmt++;
}
va_end(ap);
}
可變參數本質是「棧內存手動解析」,stdarg.h 提供了跨平台的抽象層。現代 C++ 建議優先使用 可變參數模板 或 std::format (C++20),類型安全且效能更優。但在系統級開發、嵌入式、C 兼容層場景,掌握 va_list 仍是硬功夫。