C 標準庫 <stdarg.h> 提供的 va_list、va_start、va_arg、va_end 四巨頭,讓我們能編寫接受可變數量參數的函數(如 printf、scanf)。本文以「計算任意個整數平均值」為例,詳細解析可變參數機制、棧幀佈局、型別安全陷阱與實務最佳實踐。
#include <stdio.h>
#include <stdarg.h>
// 計算任意個整數的平均值
// num: 後續參數個數
// ...: 可變參數列表
int average(int num, ...) {
int i = 0;
int sum = 0;
va_list arg; // 1. 宣告 va_list 變數
va_start(arg, num); // 2. 初始化:以最後一個固定參數 num 為基準
for (i = 0; i < num; i++) {
// 3. 依序取出參數,必須指定型別(這裡為 int)
sum += va_arg(arg, int);
}
va_end(arg); // 4. 清理(某些平台需執行)
return sum / num; // 整數除法,捨去小數
}
int main() {
int avg = average(5, 1, 2, 3, 4, 5); // 傳遞 5 個參數
printf("Average = %d\n", avg); // 輸出: 3
return 0;
}
調用: average(5, 1, 2, 3, 4, 5) 棧幀佈局 (x86 cdecl 呼叫慣例,參數從右至左壓棧): 高位址 +----------------+ | 5 | ← 第 5 個參數 | 4 | ← 第 4 個參數 | 3 | ← 第 3 個參數 | 2 | ← 第 2 個參數 | 1 | ← 第 1 個參數 | 5 | ← 固定參數 num (va_start 定位基準) +----------------+ 低位址 va_start(arg, num): arg 指向 num 之後的位址(即第一個可變參數 1 的位址) va_arg(arg, int): 1. 根據型別大小(int 4 bytes)從 arg 位址讀取值 2. 將 arg 遞增 sizeof(int) 指向下一個參數 3. 回傳讀取的值 va_end(arg): 某些架構需重置 arg 為 NULL 或執行特定清理 (x86 通常為空巨集,但為可移植性必須呼叫)
| 限制 | 說明 | 對策 |
|---|---|---|
| 無法自動得知參數個數 | 必須透過固定參數(如 num)、格式字串(如 printf)、哨兵值(如 -1)傳遞 | 設計清晰的協議:第一個參數為 count,或最後一個參數為 sentinel |
| 型別安全完全靠程式設計師 | va_arg(arg, int) 若實際傳入 double 會導致未定義行為 | 文檔化型別約定、使用格式字串驗證、編譯器警告 -Wvarargs |
| 無法驗證參數個數是否匹配 | 傳入個數少於 num 會讀取垃圾值;多於則被忽略 | 嚴格遵守協議、單元測試覆蓋邊界情況 |
| 預設參數提升 | float 自動提升為 double,char/short 提升為 int | 取用時用提升後型別:va_arg(arg, double) 取 float,va_arg(arg, int) 取 char/short |
| 不可用於 register 參數 | 某些呼叫慣例將前幾個參數放暫存器,va_list 無法存取 | 確保編譯器支援(現代編譯器通常自動處理) |
void myPrintf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
for (const char *p = fmt; *p; p++) {
if (*p != '%') { putchar(*p); continue; }
switch (*++p) {
case 'd': printf("%d", va_arg(ap, int)); break;
case 'f': printf("%f", va_arg(ap, double)); break;
case 's': printf("%s", va_arg(ap, char*)); break;
case 'c': putchar(va_arg(ap, int)); break;
}
}
va_end(ap);
}
// 以 -1 結尾,無需傳遞個數
int sumSentinel(int first, ...) {
va_list ap;
va_start(ap, first);
int sum = first;
int val;
while ((val = va_arg(ap, int)) != -1) {
sum += val;
}
va_end(ap);
return sum;
}
// 調用: sumSentinel(1, 2, 3, 4, 5, -1);
#define AVERAGE(...) _Average((int[]){__VA_ARGS__}, sizeof((int[]){__VA_ARGS__})/sizeof(int))
double _Average(int *arr, int n) {
long sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return (double)sum / n;
}
va_start/arg/end 有額外開銷。理解 va_list 機制不僅能讀懂標準庫源碼,更能在需要極致靈活介面(如日誌庫、序列化框架、腳本綁定)時,正確、安全地實現可變參數功能。