printf、scanf 等函數能接受不定數量參數,靠的是 C 語言的可變參數機制。本文從棧幀佈局原理出發,手把手實現一個簡易的 PrintInt,解析 va_list、va_start、va_arg、va_end 的標準用法與底層邏輯。
C 語言調用約定(如 x86 cdecl)規定:參數從右向左壓棧,調用者清棧。因此:
// 調用:func(fmt, a, b, c)
// 棧佈局(高位址 → 低位址)
// [fmt][a][b][c] ← 栈顶
// 首參數 fmt 位址固定,後續參數在其上方連續分佈
關鍵洞察:首參數位址已知 → 後續參數相對偏移固定。printf 掃描格式字符串,遇到 %d 就知道「下一個參數是 int」,按 sizeof(int) 遞增位址取值。
<stdarg.h> 宏機制va_list ap; // 參數指針
va_start(ap, last_fixed_param); // 定位到首個可變參數
type val = va_arg(ap, type); // 取值並推進指針
va_end(ap); // 清理(某些架構需棧指針復原)
#include
#include
#include
void PrintInt(char* buffer, int data, ...) {
if (!buffer) return;
va_list ap;
va_start(ap, data); // data 是最後一個固定參數
char temp[32];
char result[1024] = {0};
int res_len = 0;
for (int i = 0; buffer[i]; ) {
if (buffer[i] == '%' && buffer[i+1] == 'd') {
int val = va_arg(ap, int); // 取下一個 int 參數
int len = sprintf(temp, "%d", val);
memcpy(result + res_len, temp, len);
res_len += len;
i += 2; // 跳過 %d
} else {
result[res_len++] = buffer[i++];
}
}
result[res_len] = '\0';
strcpy(buffer, result);
va_end(ap);
}
// 測試
int main() {
char buf[64] = "%d + %d = %d";
PrintInt(buf, 1, 2, 3); // 注意:第一個可變參數是 1
printf("%s\n", buf); // 輸出: 1 + 2 = 3
return 0;
}
| 維度 | 原文手動計算位址版 | 標準 va_arg 版 |
|---|---|---|
| 可移植性 | 依賴特定棧佈局、參數對齊規則,極易出錯 | 編譯器處理架構差異,完全可移植 |
| 類型安全 | 手動轉型,無檢查 | 編譯期不檢查,但語義清晰 |
| 多類型支援 | 需手寫解析 %s %f %c 等 | 統一 va_arg(ap, type) 機制 |
| 浮點數傳遞 | 極難正確處理(寬度、寄存器/棧分離) | 標準庫正確處理 float→double 升級 |
void my_printf(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
char buffer[4096];
int len = vsnprintf(buffer, sizeof(buffer), fmt, ap);
if (len > 0) fwrite(buffer, 1, len, stdout);
va_end(ap);
}
核心:vsnprintf / vprintf 系列接受 va_list,是變參函數鏈式調用的標準模式。
va_start 需要最後一個固定參數名)。va_arg(ap, float) 錯誤,必須用 double。va_copy (C99)。可變參數本質是「格式字符串作為運行期類型描述符,引導棧上參數解析」。標準 <stdarg.h> 宏屏蔽了架構差異,是正確、可移植實現的唯一正道。手寫 PrintInt 是理解原理的好練習,但生產代碼務必使用 vsnprintf 等成熟設施。掌握此機制,對調試崩潰、分析棧幀、實現日誌庫、綁定腳本語言都極有幫助。