可變參數函數(Variadic Functions)讓C語言支持參數個數不固定的函數(如printf、scanf)。本文系統講解va_list、va_start、va_arg、va_end四大宏的用法、內存佈局原理、標準實現代碼、結束標志設計與安全陷阱。
#include
void func(Type fixed1, Type fixed2, ...) { // 至少一個固定參數
// 1. 聲明參數列表指針
va_list ap;
// 2. 初始化:指向第一個可變參數
va_start(ap, fixed2); // 傳入「最後一個固定參數」
// 3. 依次獲取可變參數
Type arg1 = va_arg(ap, Type); // 指定類型,自動移動到下一個
Type arg2 = va_arg(ap, Type);
// ... 直到結束標志
// 4. 清理
va_end(ap);
}
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
int s = 0;
for (int i = 0; i < count; i++) {
s += va_arg(ap, int); // 假設全為int
}
va_end(ap);
return s;
}
// 調用:sum(3, 10, 20, 30) → 60
char* concat_strings(char* first, ...) {
va_list ap;
va_start(ap, first);
// 計算總長度
size_t total = strlen(first);
const char* str = first;
while ((str = va_arg(ap, const char*)) != NULL) { // NULL作為哨兵
total += strlen(str);
}
va_end(ap);
// 分配並拼接
char* result = malloc(total + 1);
strcpy(result, first);
va_start(ap, first); // 重新開始
while ((str = va_arg(ap, const char*)) != NULL) {
strcat(result, str);
}
va_end(ap);
return result;
}
// 調用:concat_strings("Hello", " ", "World", NULL)
va_list ap, ap_copy;
va_start(ap, fmt);
va_copy(ap_copy, ap); // 複製當前狀態
// 使用ap_copy遍歷不影響ap
va_end(ap_copy);
va_end(ap);
va_start/va_copy必須配對va_end__cdecl)
調用:int TestFunc(int n1, int n2, int n3, ...);
實參壓棧順序(右到左):... , n3, n2, n1, 返回地址, 舊EBP
棧頂 ↑
// 對齊宏:將n向上取整到sizeof(int)倍數
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
// va_list 本質:char* 指向棧上參數區
typedef char* va_list;
// 初始化:ap = 最後固定參數地址 + 其對齊大小 → 指向第一個可變參數
#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)
va_start利用最後固定參數地址定位可變參數起始位置_INTSIZEOF處理內存對齊:x86要求4字節對齊,參數佔用空間向上取整va_arg「先遞增後解引用」技巧:ap += size; return *(t*)(ap - size);va_arg靠程序員指定類型強制轉換,類型錯誤→未定義行為"%d %s %f"提供類型信息,運行時解析決定va_arg類型%d → va_arg(ap, int),%s → va_arg(ap, char*),%f → va_arg(ap, double)(float提升為double)
int vspf(char *buf, size_t size, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int cnt = vsnprintf(buf, size, fmt, ap); // 直接傳遞va_list
va_end(ap);
return cnt;
}
vsnprintf接受va_list而非...,是vsprintf安全版_vsnprintf(行為略異)| 陷阱 | 後果 | 對策 |
|---|---|---|
| 類型不匹配 | va_arg(ap, int)讀double → 位模式錯誤 | 格式字符串強制類型;C++用模板/重載替代可變參數 |
| 缺少哨兵/越界讀取 | 讀取無效棧內存 → 崩潰/信息洩露/代碼執行 | 必須有明確結束條件:計數參數、哨兵值、格式串指示 |
// 1. 初始化列表(同類型)
void print_all(std::initializer_list il) {
for (int x : il) std::cout << x << ' ';
}
// 2. 可變參數模板(異構類型,類型安全)
template
void print_all(Args&&... args) {
(std::cout << ... << args) << '\n'; // C++17 折疊表達式
}
// 3. fmt/std::format(格式化,類型安全)
std::string fmt_str = std::format("{}, {}!", "Hello", "World"); // C++20
// 4. std::vector/span 替代數組指針+長度
void process(std::span data);
<stdarg.h>(C++用<cstdarg>)va_start錨點va_start(ap, last_fixed_param)傳最後一個固定參數名va_arg(ap, Type)類型必須與實際傳入完全一致(含符號性、寬度)va_start/va_copy必配對va_end可變參數機制是C語言「信任程序員」哲學的極致體現:極簡的棧指針操作換取極大的靈活性,但也將類型安全、邊界檢查完全拋給程序員。現代代碼應盡量避免直接使用,改用模板、重載、標準庫設施實現同等表達力且編譯期保證安全。