1. 簡介
可否想過 C 語言中最常見常用的 printf() 函數是如何做到接收任意類型、任意數量的參數的呢?
實際上,printf() 中通過可變參數特性來接收任意類型、任意數量的參數。可變參數通過佔位 ...
顯式指示,例如以下函數的形參就是可變參數:
void fun (...)
{
// do something
}
2. 相關 API
在函數中怎麼使用這些可變參數呢?C 語言在頭文件 <stdarg.h> 中提供了三個操作可變參數的 API:va_start
、va_arg
、va_end
,前綴 va 即 variable argument(可變參數)。
這三個 API 都爲函數宏,功能如下:
- va_start
va_start
的聲明如下(僞代碼):
void va_start (va_list ap, last);
ap
爲可變參數列表對象,需要在調用 va_start
前創建。va_list
內包含一個成員指針,即 ap
內有指向可變參數的指針。
last
爲可變參數的佔位符前的形參。一般情況下,使用可變參數的函數需要提供一個形參用於函數調用時指定可變參數的個數,否則在程序中將無法得知具體的可變參數個數。
va_start
用於初始化可變參數列表對象,把 ap
內的可變參數指針指向 last
的下一個參數即可變參數列表的第一個參數。
- va_arg
va_arg
的聲明如下(僞代碼):
type va_arg (va_list ap, type);
type
爲可變參數的數據類型。
va_arg
用於獲取 ap
的可變參數參數指針所指的參數並作爲返回值。獲取後該指針將根據 type
偏移一定字節數,指向下一個可變參數。
需要注意的是,使用 GCC 編譯時,當可變參數類型爲 char 或 short 即少於四個字節,可變參數將佔據四個字節大小。因此,此時的函數實參 type
應顯式指示爲 int。
- va_end
va_end
的聲明如下(僞代碼):
void va_end (va_list ap);
va_end
用於釋放可變參數列表對象,需要在函數最後調用完成收尾工作。如果在釋放後繼續操作可變參數列表對象,結果是未知的。
3. 應用方法
前文提到,一般情況下,使用可變參數的函數需要提供一個形參用於函數調用時指定可變參數的個數。
以以下求和函數爲例:
int sum(int n, ...)
{
int sum = 0;
va_list valist;
va_start(valist, n);
while (n--)
{
sum += va_arg(valist, int);
}
va_end(valist);
return sum;
}
調用 sum() 函數時,指定可變參數的個數 n
。在函數中,可以調用 n 次 va_arg
獲取所有傳遞的可變參數。
但對於 printf() 而言,調用函數時並沒有指定 n
,那麼它是怎樣得知可變參數個數的呢?實際上,printf() 內會根據給定字符串中格式爲 %<...>
的佔位符推算出可變參數的個數。
現可實現簡單的 printf() 函數:
int MyPrint(const char* s, ...)
{
if (s == NULL)
{
return -1;
}
va_list valist;
va_start(valist, s);
while (*s)
{
if (*s == '%')
{
s++;
switch (*s)
{
case 'c':
{
putchar(va_arg(valist, int));
break;
}
case 's':
{
const char* p = va_arg(valist, char*);
while (*p)
{
putchar(*p++);
}
break;
}
define :
{
va_end(valist);
return -1;
}
}
s++;
}
else
{
putchar(*s);
s++;
}
}
va_end(valist);
return 0;
}
在程序中使用 MyPrint():
int main(int argc, char **argv)
{
MyPrint("Hello World!\n");
char* s = "123456";
MyPrint("%s%s\n", s, "789");
}
編譯後,運行結果如下:
kong@ubuntu:/mnt/hgfs/share/pj_cpp$ output/app
Hello World!
123456789
kong@ubuntu:/mnt/hgfs/share/pj_cpp$
4. 實現原理
可變參數的實現與編譯器強相關,不同編譯器的實現方式很可能不一樣。例如有的 32 位編譯器中可變參數都存放在棧上,而有的 64 位編譯器則將部分可變參數存放到寄存器上。
更詳細的介紹有可見:揭密X86架構C可變參數函數實現原理