在某些情况下我们希望函数参数的个数可以根据实际需要来确定,所以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++中更为安全的方式来完美代替之。