前言
C 語言透過 `` 宏實作可變參數函數 (如 `printf`)。本文深入解析 `va_list`、`va_start`、`va_arg`、`va_end` 機制、堆疊模型、使用陷阱與最佳實踐。
核心宏與類型
#include
void func(int fixed, ...) {
va_list ap; /* 參數指針,實為結構體/指針 */
va_start(ap, fixed); /* 定位至第一個可變參數 */
int val = va_arg(ap, int); /* 取得下一參數,ap 前進 */
// ...
va_end(ap); /* 清理 (部平台為空) */
}
運作原理 (x86 呼叫慣例)
- 參數由右至左壓入堆疊 (或暫存器 + 堆疊)。
- `va_start(ap, last_fixed)` 讓 `ap` 指向第一個可變參數在堆疊上的位址。
- `va_arg(ap, type)` 根據 `type` 大小從 `ap` 讀取資料,並將 `ap` 增加 `sizeof(type)` (對齊調整)。
- `va_end(ap)` 重置 `ap` (部分平台為空操作)。
完整範例:可變參數求和 (以 0 為結束標記)
#include
#include
void sum(int first, ...) {
va_list ap;
va_start(ap, first);
int n = first, total = 0;
while (n != 0) {
total += n;
n = va_arg(ap, int);
}
va_end(ap);
printf("Sum = %d\n", total);
}
int main() {
sum(10, 20, 30, 40, 0); /* 輸出 100 */
return 0;
}
關鍵限制與陷阱
- 無法自動識別參數個數/類型:需約定結束標記 (如 0、NULL、格式字串 `%d`)。
- 預設引數提升:`float` → `double`,`char`/`short` → `int`。`va_arg(ap, float)` **錯誤**,必須用 `double`。
- 參數不可為 register/陣列/函數:需取址,`va_start` 需位址。
- 型別不匹配導致未定義行為:`va_arg(ap, long)` 讀 `int` 參數會誤讀相鄰位元組。
- 編譯器原型檢查有限:可變參數部分跳過型別檢查,易藏 Bug。
編譯器實作差異
- GCC x86-64:前 6 個整數/指標參數經 `RDI, RSI, RDX, RCX, R8, R9` 傳遞,超過部分用堆疊。`va_list` 為結構體含暫存器緩衝區指標、堆疊指標、計數器。
- ARM64:類似,暫存器 `X0-X7`。
- `va_end` 在 GCC x86-64 定義為 `ap = (va_list)0`,不產生指令。
C++ 替代方案
- 函數多載 / 預設參數
- 模板可變參數 (C++11 `variadic templates`) — 型別安全、編譯期展開。
- `std::initializer_list` / `std::variant` / `std::any`
總結
可變參數宏提供極大靈活性,但犧牲型別安全與自省能力。**僅在必要時使用** (如格式化輸出、日誌),優先考慮 C++ 型別安全替代方案或固定參數 + 結構體/陣列。