1. 程式人生 > >儘量不要使用可變引數

儘量不要使用可變引數

在某些情況下我們希望函式引數的個數可以根據實際需要來確定,所以C語言中就提供了一種長度不確定的引數,形如:“...”,C++語言也繼承了這一語言特性。在採用ANSI標準形式時,引數個數可變的函式的原型是:
typefuncname(typepara1,typepara2,...);

這種形式至少需要一個普通的形式引數,後面的省略號(...)不能省去,它是函式原型必不可少的一部分。典型的例子有大家熟悉的printf()、scanf()函式,如下所示的就是printf()的原型:
intprintf(constchar*format,...);

除了引數format固定以外,其他引數的個數和型別是不確定的。在實際呼叫時可以有以下形式:
intyear=2011;
charstr[]="Hello2011";
printf("Thisyearis%d",year);
printf("Thegreetingwordsare%s",str);
printf("Thisyearis%d,andthegreetingwordsare:%s",year,str);

也許這些已經為大家所熟知,但是可變引數的實現原理卻是C語言中比較難理解的一部分。在標準C語言中定義了一個頭檔案,專門用來對付可變引數列表,其中,包含了一個va_list的typedef宣告和一組巨集定義va_start、va_arg、va_end,如下所示:
//File:VC++2010中的stdarg.h
#include<vadefs.h>
#defineva_start_crt_va_start
#defineva_arg_crt_va_arg
#defineva_end_crt_va_end
//File:VC++2010中的vadefs.h
#ifndef_VA_LIST_DEFINED
typedefchar*va_list;
#define_VA_LIST_DEFINED
#endif
#ifdef__cplusplus
#define_ADDRESSOF(v)(&reinterpret_cast<constchar&>(v))
#else
#define_ADDRESSOF(v)(&(v))
#endif
#ifdefined(_M_IX86)
#define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
#define_crt_va_start(ap,v)(ap=(va_list)_ADDRESSOF(v)+_INTSIZEOF(v))
#define_crt_va_arg(ap,t)(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))
#define_crt_va_end(ap)(ap=(va_list)0)

定義_INTSIZEOF(n)是為了使系統記憶體對齊;va_start(ap,v)使ap指向第一個可變引數在堆疊中的地址,va_arg(ap,t)使ap指向下一個可變引數的堆疊地址,並用*取得該地址的內容;最後變參獲取完畢,通過va_end(ap)讓ap不再指向堆疊,如圖1-3所示。

由於將va_start、va_arg、va_end定義成了巨集,可變引數的型別和個數在該函式中完全由程式程式碼控制,並不能智慧地進行識別,所以導致編譯器對可變引數的函式原型檢查不夠嚴格,難於查錯,不利於寫出高質量的程式碼。

引數個數可變具有很多的優點,為程式設計師帶來了很多的方便,但是上面C風格的可變引數卻存在著如下的缺點:

(1)缺乏型別檢查,型別安全性無從談起。“省略號的本質是告訴編譯器‘關閉所有檢查,從此由我接管,啟動reinterpret_cast’”,強制將某個型別物件的記憶體表示重新解釋成另
外一種物件型別,這是違反“型別安全性”的,是大忌。

例如,自定義的列印函式。

voidUserDefinedPrintFun(char*format,inti,...)
{
va_listarg_ptr;
char*s=NULL;
int*i=NULL;
float*f=NULL;
va_start(arg_ptr,i);
while(*format!='\0')
{
format++;
if(*(format-1)=='%'&&*format=='s')
{
s=va_arg(arg_ptr,char*);
……//輸出至螢幕
}
elseif(*(format-1)=='%'&&*format=='d')
{
i=va_arg(arg_ptr,int*);
……//輸出至螢幕
}
elseif(*(format-1)=='%'&&*format=='f')
{
f=va_arg(arg_ptr,float*);
……//輸出至螢幕
}
}

va_end(arg_ptr);
return;
}

如果採用下面三種方法呼叫,合法合理:
UserDefinedPrintFun("%d",2010);//結果2010
UserDefinedPrintFun("%d%d",2010,2011);//結果20102011
UserDefinedPrintFun("%s%d","Hello",2012);//結果Hello2012

但是,當給定的格式字串與引數型別不對應時,強制轉型這個“怪獸”就會被喚醒,悄悄地毀壞程式的安全性,這可不是什麼高質量的程式,如下所示:
UserDefinedPrintFun("%d",2010.80f);
//結果2010
UserDefinedPrintFun("%d%d","Hello",2012);
//結果150958722015(這是什麼結果???)

(2)因為禁用了語言型別檢查功能,所以在呼叫時必須通過其他方式告訴函式所傳遞引數的型別,以及引數個數,就像很多人熟知的printf()函式中的格式字串char*format。這種方式需要手動協調,既易出錯,又不安全,上面的程式碼片段已經充分說明了這一點。

(3)不支援自定義資料型別。自定義資料型別在C++中佔有較重的地位,但是長引數只能傳遞基本的內建型別。還是以printf()為例,如果要打印出一個Student型別物件的內容,對於這樣的自定義型別,該用什麼格式的字串去傳遞引數型別呢?如下所示:
classStudent
{
public:
Student();
~Student();
private:
stringm_name;
charm_age;
intm_scoer;
};
StudentXiaoLi;
printf(format,XiaoLi);//format應該是什麼呢

上述缺點足以讓我們有了拒絕使用C風格可變引數的念頭,何況C++的多型性已經為我們提供了實現可變引數的安全可靠的有效途徑呢!如下所示:
classPrintFunction
{
public:
voidUserDefinedPrintFun(inti);
voidUserDefinedPrintFun(floatf);
voidUserDefinedPrintFun(inti,char*s);
voidUserDefinedPrintFun(floatf,char*s);
private:
……
};

雖然上述設計不能像printf()函式那樣靈活地滿足各種各樣的需求,但是可以根據需求適度擴充函式定義,這樣不僅能滿足需求,其安全性也是毋庸置疑的。舍安全而求危險,這可不是明白人所為。如果還對printf()的靈活性念念不忘,我告訴大家,有些C++庫已經使用C++高階特性將型別安全、速度與使用方便很好地結合在一起了,比如Boost中的format庫,大家可以嘗試使用。

請記住:

編譯器對可變引數函式的原型檢查不夠嚴格,所以容易引起問題,難於查錯,不利於寫出高質量的程式碼。所以應當儘量避免使用C語言方式的可變引數設計,而用C++中更為安全的方式來完美代替之。