在某些情況下我們希望函數參數的個數可以根據實際需要來確定,所以C語言中就提供了一種長度不確定的參數,形如:“…”,通過將函數實現爲可變參數的形式,可以使得函數可以接受1個以上的任意多個參數。
典型的例子有printf()、scanf()函數等,下面就用printf函數的原型爲例分析:
int printf( const char *format [, argument]... );
如上,除了參數format固定以外,其他參數的類型和個數是不確定的。在實際調用的時候有如下類型:
printf("%d",num);
printf("%s",num);
printf("%c",num);
...
在標準C語言中定義了一個頭文件,專門用來對付可變參數列表,其中,包含了一個va_list的typedef聲明和一組宏定義va_start、va_arg、va_end,如下所示:
va_list arg;
va_start(arg, n);
va_arg(arg, (數據類型) );
va_end(arg);
va_list:聲明一個va_list類型的變量arg,它可以訪問參數列表的未確定部分。
這個變量是調用va_start來初始化的。它的第一個參數是va_list的變量名,第2個參數是省略號前最後一個有名字的參數。初始化過程把arg變量設置爲指向可變參數部分的第一個參數。
va_arg:這個宏和接受兩個參數,va_list變量和參數列表中下一個參數的類型。在這個例子中所有的可變參數都是整型。va_arg返回這個參數的值,並使用va_arg指向下一個可變參數。
va_end:訪問完畢最後一個可變參數,通過va_end(ap)讓ap不再指向堆棧。
例:自定義的打印函數
int my_printf(char *str, ...)
{
va_list arg;
char* str_tmp = NULL;
char buf[10] = {0};
va_start(arg, str);
while (*str != '\0')
{
switch (*str)
{
case 's':
str_tmp = (char*)va_arg(arg, int);//取下一個參數的地址,因爲這個是字符串
while (*str_tmp != '\0')//利用解引用進行輸出
{
putchar(*str_tmp);
str_tmp++;
}
break;
case 'c':
putchar(va_arg(arg, int));
break;
case 'd':
int d = va_arg(arg, int);
_itoa(d, buf, 10);
for (str_tmp = buf; *str_tmp; str_tmp++)
{
putchar(*str_tmp);
}
break;
case '\n':
puts(" ");
break;
default:
;
break;
}
str++;
}
va_end(arg);
return 0;
}
int main()
{
my_printf("s ccc d.\n", "hello", 'z', 'z', 'z','520');
system("pause");
return 0;
}
由於將va_start、va_arg、va_end定義成了宏,可變參數的類型和個數在該函數中完全由程序代碼控制,並不能智能地進行識別,所以導致編譯器對可變參數的函數原型檢查不夠嚴格,難於查錯,不利於寫出高質量的代碼。
——《編寫高質量代碼》
雖然參數可變爲程序員帶來了很多便利,但也有一些不可避免的缺陷。比如:
1.缺乏類型檢查,類型安全性不能保證。
2.因爲禁用了語言類型檢查功能,所以在調用時必須通過其他方式告訴函數所傳遞參數的類型。
3.不支持自定義數據類型。
以上,因爲編譯器對可變參數函數的原型檢查不夠嚴格,所以容易引起問題,難於查錯,不利於寫出高質量的代碼。所以應當儘量避免使用C語言方式的可變參數設計。