不定參數是 C/C++ 的特色功能,除格式化輸出(printf)外鮮見應用。主因在於使用繁瑣、副作用多,且函數多載通常足以替代。本文系統梳理其運作機制、關鍵巨集與常見坑點。
設計不定參數函數需解決三大問題:
採用 cdecl 慣例(全域函數)或 thiscall(成員函數)強制要求:
f(a,b,c) 先壓 c,再 b,最後 a)。printf)固定參數中包含格式字串,解析其中的格式指示符(%d, %s…)推導後續參數個數與型別。
sumi)適用於同型別參數,約定特定值(如 0)作為結束標記:
int sumi(int c, ...) {
va_list ap; va_start(ap, c);
int sum = c;
while ((c = va_arg(ap, int)) != 0) sum += c;
return sum;
}
// 呼叫:sumi(1,2,3,4,5,6,7,8,9,0); // 以 0 結束
va_start / va_arg / va_end以 VC6 為例,巨集定義揭示底層邏輯:
#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)))
_INTSIZEOF 計算向上對齊至 sizeof(int) 倍數的位元組數。va_start 讓 ap 指向固定參數 v 之後、第一個 4 位元組對齊的位址。va_arg 依型別 t 取值並將 ap 推進到下一個參數。int func(int a, int b, ...);
int func(int a, int b, float c); // 錯誤:func(1,2,3.3) 歧義
編譯器無法判斷第三個參數屬於不定參數還是 float 多載。建議不定參數函數不進行多載。
int sum = sumi(1, 2.2, 3, 0); // 錯誤:傳入 double 但 va_arg 取 int
va_arg(ap, int) 直接將堆疊位址強轉為 int* 解參考。若實際傳入 double(8 位元組,IEEE 754 表示),位元組模式與 int 截然不同,導致讀取垃圾值。呼叫端必須嚴格符合被呼叫端預期的型別,編譯器不會執行隱式轉換。
std::initializer_list (C++11) 或可變參數模板 (C++11) 替代。-Wformat 警告)。fmt 函式庫風格)。va_end(ap) 以符合標準(雖然 VC 多為空巨集)。理解不定參數的堆疊佈局與巨集展開邏輯,有助於除錯疑難崩潰與撰寫更安全的介面。