在閱讀 Linux 內核原始碼時,經常會遇到 vsprintf 這類使用可變參數的函數。C 語言透過 <stdarg.h> 提供了一套標準化的機制來處理參數數量不固定的函數。本文將從基礎用法、底層實現原理、常見陷阱到最佳實踐,系統性地講解可變參數的核心知識。
要使用可變參數,必須包含 <stdarg.h> 並掌握三個核心巨集:
va_start(va_list ap, last_fixed_arg):初始化參數列表指標va_arg(va_list ap, type):按指定型別取出下一個參數va_end(va_list ap):清理參數列表#include <stdio.h>
#include <stdarg.h>
void simple_va_fun(int fixed, ...) {
va_list ap;
int optional = 0;
va_start(ap, fixed); // 以最後一個固定參數為基準
optional = va_arg(ap, int); // 取出第一個可變參數 (int)
va_end(ap);
printf("fixed=%d, optional=%d\n", fixed, optional);
}
int main(void) {
simple_va_fun(100); // 輸出: fixed=100, optional=隨機垃圾值
simple_va_fun(100, 200); // 輸出: fixed=100, optional=200
simple_va_fun(100,200,300);// 輸出: fixed=100, optional=200 (第三個被忽略)
return 0;
}
關鍵步驟總結:
va_list 變數作為參數指標va_start 初始化,第二參數必須是最後一個「固定」參數va_arg 取得各參數,第二參數指定預期型別va_end 結束stdarg.h 中的巨集實作與硬體平台、呼叫慣例緊密相關。以 x86 平台 VC++ 為例:
typedef char *va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#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)
C 語言參數從右向左壓入堆疊(高位址 → 低位址):
高位址 |-----------------------------|
| 函數返回位址 |
|-----------------------------|
| ... |
|-----------------------------|
| 第 n 參數 (第一個可變參數) | ← va_start 後 ap 指向此處
|-----------------------------|
| 第 n-1 參數 (最後固定參數) | ← &v
低位址 |-----------------------------|
va_start:將 ap 設為「最後固定參數位址 + 其對齊後大小」,指向第一個可變參數va_arg:先將 ap 增加目標型別大小(對齊後),再回傳增加前位址的值va_end:將 ap 歸零,防止誤用注意: 因為取參數位址,最後一個固定參數不能是暫存器變數、陣列或函數型別。
巨集本質是編譯期文字替換,完全不知道呼叫端傳了幾個、什麼型別的參數。printf 之所以能「智慧解析」,是因為它透過格式字串(%d、%s…)在執行期決定如何呼叫 va_arg。
對策: 自行設計協定,常見兩種:
NULL、-1)作為結束標記void bad_fun(int n, ...) {
va_list ap;
va_start(ap, n);
char *s = va_arg(ap, char*); // 呼叫端若傳 int,這裡會取錯資料
va_end(ap);
}
編譯器對 ... 的型別檢查極其寬鬆,極易在執行時造成崩潰或難以重現的 Bug。
舊式 <varargs.h> 的 va_start 僅有一參數,與 ANSI C 兩參數版本不相容。現代程式碼一律使用 <stdarg.h>(ANSI C 標準)。
#include <stdio.h>
#include <stdarg.h>
#define TYPE_INT 1
#define TYPE_STR 2
#define TYPE_DOUBLE 3
void print_mixed(int count, ...) {
va_list ap;
va_start(ap, count);
for (int i = 0; i < count; ++i) {
int type = va_arg(ap, int); // 先取型別標記
switch (type) {
case TYPE_INT:
printf("int: %d\n", va_arg(ap, int));
break;
case TYPE_STR:
printf("str: %s\n", va_arg(ap, char*));
break;
case TYPE_DOUBLE:
printf("double: %f\n", va_arg(ap, double));
break;
}
}
va_end(ap);
}
int main(void) {
// 傳遞:個數, 型別1, 值1, 型別2, 值2...
print_mixed(3, TYPE_INT, 42, TYPE_STR, "hello", TYPE_DOUBLE, 3.14);
return 0;
}
在 C++ 中,優先考慮:
variadic templates)實現型別安全的可變參數printf 風格僅在與 C API 介接、或極度效能敏感的底層程式碼中使用 stdarg.h。
va_list、va_start、va_arg、va_end 是處理可變參數的四件套