C 語言的可變參數機制(<stdarg.h>)是實現 printf、scanf 等格式化輸入輸出函數的核心。本文通過一個封裝 vsnprintf 的 myprintf 範例,詳細講解 va_list、va_start、va_end 的正確使用姿勢,並指出常見坑點。
#include <stdarg.h>
#include <stdio.h>
char buff[80]; // 全域緩衝區(注意:非執行緒安全)
int myprintf(char *format, ...) {
va_list ap;
va_start(ap, format); // 初始化,format 為最後一個固定參數
int num = vsnprintf(buff, sizeof(buff), format, ap); // 格式化寫入緩衝區
va_end(ap); // 清理
return num; // 返回寫入字元數(不含 \0)
}
int main() {
int a = 23;
float b = 23.0011;
char c = 'd';
myprintf("%d %f %c", a, b, c);
printf("%s", buff); // 輸出: 23 23.001100 d
return 0;
}
va_list ap; 定義一個變量參數列表對象,本質上是一個指針,指向棧上第一個可變參數的地址。
va_start(ap, format); 以最後一個固定參數 format 為基準,計算出第一個可變參數的地址並賦給 ap。必須傳入最後一個固定參數名,否則編譯器無法定位棧佈局。
va_arg(ap, type) 依序取出參數,需自行匹配格式字串,易出錯。ap 傳給 vsnprintf、vprintf 等標準庫函數,由它們負責解析格式與參數,安全且高效。va_end(ap); 執行必要的清理工作(某些架構下需重置棧指針),每個 va_start 必須配對一個 va_end,否則未定義行為。
範例中 buff 為全域靜態陣列,多執行緒同時調用 myprintf 會導致資料競爭。實際專案建議:
Thread-local storage(C11 thread_local 或編譯器擴展)。va_list 在某些架構(如 x86-64)上是結構體而非簡單指針,傳遞給 v* 函數後內部狀態會改變。若需二次遍歷參數,必須用 va_copy(dest, src)(C99)複製一份。
編譯器無法檢查可變參數類型,%d 對應 float 會導致未定義行為。開啟編譯器警告(-Wformat)並嚴格對應。
掌握 va_start / va_arg / va_end 三部曲,並善用 vsnprintf 等 v* 系列函數,即可安全、靈活地構建自己的格式化輸出函數,這也是系統級庫開發、日誌框架、嵌入式 Shell 實現的基礎技能。