可變參數 (...) 是 C 語言實現 printf、scanf 等函數的基石。本文系統梳理 棧帧手動解析、標準 stdarg.h、自定義簡易 printf、編譯器內建宏 四種層次的實現原理與代碼模式,幫助開發者從「會用」進階到「懂原理、能造輪子」。
void fun(int a, ...) {
int *temp = &a; // 獲取第一個固定參數地址
temp++; // 移動到下一個棧槽 (可變參數起始)
for (int i = 0; i < a; ++i) { // a 代表可變參數個數
printf("%d\n", *temp);
temp++; // 逐個讀取 (假設全為 int)
}
}
int main() {
fun(4, 1, 2, 3, 4); // 輸出 1 2 3 4
}
缺陷:假設參數類型統一、大小固定、棧增長方向、調用約定、無寄存器傳參 (x64 完全失效)、**極不安全,僅供原理理解**。
stdarg.h:可移植的標準解法| 宏 | 作用 |
|---|---|
va_list ap |
定義參數列表指針 (不透明類型) |
va_start(ap, last_fixed) |
初始化 ap 指向第一個可變參數 |
va_arg(ap, type) |
取出當前參數 (按 type 大小),並推進 ap 到下一個 |
va_end(ap) |
清理 ap (置 NULL),必須成對調用 |
va_copy(dest, src) |
C99:複製參數列表,支持多次遍歷 |
#include
#include
#include
int demo(char *msg, ...) {
va_list ap;
va_start(ap, msg);
int argno = 0;
char *para;
while ((para = va_arg(ap, char*)) != NULL) { // 約定:以 NULL 結束
if (strcmp(para, "") == 0) break; // 或用空字符串作為終止符
printf("Parameter #%d is: %s\n", argno++, para);
}
va_end(ap);
return 0;
}
int main() {
demo("DEMO", "This", "is", "a", "demo!", ""); // 以空字符串終止
}
va_start 的第二參數 (定位棧帧基準)。printf 通過格式字符串隱式傳遞),va_arg 無法自動推導類型。va_copy 複製 ap,每次遍歷獨立 va_start/va_end。myprintf:格式化解析核心邏輯
void myprintf(char *fmt, ...) {
char *pArg = (char *)&fmt;
pArg += sizeof(fmt); // 跳過 fmt 指針自身,指向第一個可變參數 (假設棧佈局)
char c;
while ((c = *fmt++) != '\0') {
if (c != '%') {
putchar(c);
} else {
switch (*fmt++) {
case 'd':
printf("%d", *((int *)pArg)); // 強轉讀取 int
pArg += sizeof(int);
break;
// 可擴展 %s %c %f 等
}
}
}
}
局限:同樣依賴棧佈局假設,不處理對齊、寄存器傳參、浮點、長度修飾符等,**不可用於生產**。
stdarg.h 源碼級還原)
// 簡化版 MSVC 實現邏輯
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) // 向上對齊到 int 倍數
#define va_start(ap, v) ( ap = (va_list)&v + _INTSIZEOF(v) )
// &v 取最後固定參數地址,加上其對齊後大小,得到第一個可變參數地址
#define va_arg(ap, t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
// 1. ap 增加 t 對齊後大小 → 指向下一個參數
// 2. 減去同量 → 指向當前參數
// 3. 強轉為 t* 解引用取值
#define va_end(ap) ( ap = (va_list)0 )
#include
#include
using namespace std;
const int N = 5;
void Stdarg(int a1, ...) {
va_list ap;
int ary[N];
va_start(ap, a1);
ary[0] = a1;
for (int i = 1; i < N; i++)
ary[i] = va_arg(ap, int);
va_end(ap);
for (int i = 0; i < N; i++)
cout << ary[i] << endl;
}
int main() {
Stdarg(5, 12, 64, 34, 23); // 輸出 5 12 64 34 23
}
va_list 在 x64 下通常是**結構體**,包含:寄存器保存區指針、棧溢出區指針、當前寄存器索引等。stdarg.h。
template
void log(Args&&... args) { (cout << ... << args) << '\n'; }
_Generic + 宏:有限度類型分發。std::format:類型安全格式化,編譯期檢查格式字符串。va_arg(ap, float) → **錯誤**!float 提升為 double,必須用 va_arg(ap, double)。va_end → 資源洩漏 / 未定義行為。va_list 多次遍歷未 va_copy → 指針已推進到末尾,二次遍歷讀垃圾。std::format / fmt::format / 可變參數模板,僅在 C 接口兼容層用 stdarg.h。可變參數的本質是 調用約定下的棧/寄存器佈局遍歷。從手動指針算術理解原理,到掌握 stdarg.h 標準接口,再到認識 x64 下的結構體 va_list 與現代類型安全替代方案,這條學習路徑完整覆蓋了 C/C++ 可變參數的過去、現在與未來。**生產環境請拋棄手動解析,擁抱標準庫與現代 C++。**