C 語言通過省略號 ... 支持可變參數函數,這是 printf、scanf 等標準庫函數實現多態調用的基石。本文系統講解可變參數的聲明規則、標準處理宏(va_list 等)、實戰示例,並深入探討宏定義中 ##__VA_ARGS__ 的高級用法與常見陷阱。
在函數原型中,固定參數之後使用 ... 表示後續參數個數與類型不固定:
int mult(int x, int y, const char *fmt, ...);
編譯器檢查規則:僅對固定參數(此處 x、y、fmt)進行類型檢查;對 ... 部分完全不檢查類型與個數,調用時傳入任意數量、任意類型的實參均合法。
<stdarg.h> 宏集在函數內部訪問可變參數必須使用以下四個宏,嚴禁直接通過指針算術訪問棧內存(不具可移植性):
| 宏 | 作用 |
|---|---|
va_list ap; | 聲明一個變量,用於追蹤可變參數列表位置 |
va_start(ap, last_fixed_arg); | 初始化 ap 指向第一個可變參數,last_fixed_arg 是最後一個固定參數名 |
type va_arg(ap, type); | 按指定 type 取出下一個參數,並推進 ap |
va_end(ap); | 清理 ap,必須在函數返回前調用 |
#include <stdio.h>
#include <stdarg.h>
void safe_print(const char *prefix, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
if (prefix) printf("[%s] ", prefix);
vprintf(fmt, ap); // 關鍵:使用 vprintf 等 v-系列函數處理 va_list
va_end(ap);
}
int main() {
safe_print("INFO", "User %s logged in, ID=%d\n", "alice", 1001);
safe_print("WARN", "Disk usage: %.1f%%\n", 87.5);
return 0;
}
##__VA_ARGS__ 技巧C99 標準化了宏的可變參數 __VA_ARGS__。GCC/Clang 擴展引入 ## 運算符,解決「零可變參數時殘留逗號」的問題:
// 無 ##:LOG("start") 展開為 fprintf(stderr, "start", ) → 語法錯誤
#define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
// 有 ##:LOG("start") 展開為 fprintf(stderr, "start") → 正確
#define LOG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
// 多層宏傳遞示例(來自原文,經整理)
#define rectangle_area(x, y, args...) mult(x, y, ##args)
#define square_area(x, args...) rectangle_area(x, x, ##args)
#define myprintf(args...) square_area(1, ##args)
// 調用鏈:myprintf("hello", "world")
// → square_area(1, "hello", "world")
// → rectangle_area(1, 1, "hello", "world")
// → mult(1, 1, "hello", "world")
// mult 只使用前三個參數,其餘被 ... 吸收忽略
原文示例中 myprintf("haha", "666", 12345) 最終僅輸出 haha,原因在於:
##args 將參數包原樣透傳給下一層的 args...。mult 的簽名為 (int, int, const char*, ...),故只有第三個參數(第一個可變參數)被 fmt 接收,後續所有參數歸入 ... 被忽略。fmt 或 count),否則無法定位可變參數起始位置。va_arg(ap, double) 取 float 會觸發 UB(默認參數提升規則下 float→double,故須用 double)。template<typename... Args>),提供類型安全與編譯期檢查。可變參數是 C 語言表達靈活性的利器,但伴隨著類型安全的妥協。掌握 <stdarg.h> 標準流程、理解宏定義中 ##__VA_ARGS__ 的展開規則、認清「參數被靜默忽略」的風險,是編寫健壯 C 代碼的必修課。