1、概述
C标准中有一个默认参数提升(default argument promotions)规则。
默认参数提升有时会给我们带来疑惑。本文结合C语言的可变长参数函数来说明默认参数提升存在的陷阱。
2、默认参数提升的定义
标准中的定义如下:
If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. -- C11 6.5.2.2 Function calls (6)
意思大概是:如果一个函数的形参类型未知, 例如使用了Old Style C风格的函数声明,或者函数的参数列表中有 ...,那么调用函数时要对相应的实参做Integer Pormotion,此外,相应的实参如果是float型的也要被提升为double类型,这条规则称为Default Argument Promotion。
3、可变长参数函数
熟悉C的人都知道,C语言支持可变参长数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用(...)表示,比如我们常用的printf()\execl函数等;printf函数的原型如下:
注意,采用这种形式定义的可变参数函数,至少需要一个普通的形参,比如上面代码中的*format,后面的省略号是函数原型的一部分。
C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_start、va_arg和va_end;在ANSI C标准下,这些宏定义在stdarg.h中。三个宏的原型如下:
当一个函数被定义为可变参数函数时,其函数体内首先要定义一个va_list的结构体类型,这里沿用原型中的名字,ap。va_start使ap指向第一个可选参数。va_arg返回参数列中的当前参数并使ap指向参数列表中的下一个参数。va_end把ap指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。
下面是一个具体的示例:
4、默认参数提升在可变参数函数的陷阱
如果明白了C语言的可变参数函数,让我们实现一个简易的my_printf
1. 它只返回void, 不记录输出的字符数目
2. 它只接受"%d"按整数输出、"%c"按字符输出、"%%"输出'%'本身
很多人的答案如下:
很可惜,这样的代码是错误的!
简单的说,我们用va_arg(ap,type)取出一个参数的时候,
type绝对不能为以下类型:
——char、signed char、unsigned char
——short、unsigned short
——signed short、short int、signed short int、unsigned short int
——float
一个简单的理由是:
——调用者绝对不会向my_printf传递以上类型的实际参数。
为什么呢?-- 这里就牵扯到默认参数提升问题。
看标准:
If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualied versionof its declared type.The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments. -- C11 6.5.2.2 Function calls (7)
C语言中什么时候会牵扯到默认参数提升呢?
在C语言中,调用一个不带原型声明的函数时:调用者会对每个参数执行“默认实际参数提升(default argument promotions)。
同时,对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到double
——char、short和相应的signed、unsigned类型的实际参数提升到int
——如果int不能存储原值,则提升到unsigned int
然后,调用者将提升后的参数传递给被调用者。
所以,my_printf是绝对无法接收到上述类型的实际参数的。
上面的代码的42与43行,应该改为:
int c = va_arg(ap,int);
printf("%c",c);
同理, 如果需要使用short和float, 也应该这样:
short s = (short)va_arg(ap,int);
float f = (float)va_arg(ap,double);
再来看一个具体的例子吧:
上面的代码用gcc4.4.0编译,会有警告:
运行gcc4.4.6生成的程序时,运行到第23行时,输出Illegal instruction,程序退出。查看了一下gcc4.4.6生成的汇编代码,发现没有为read_args_from_va_bad()生成有效的代码。
UD2是一种让CPU产生invalid opcode exception的软件指令. 内核发现CPU出现这个异常, 会立即停止运行
在VC中运行的结果是不正确的:
以下摘自《C陷阱与缺陷》
这里有一个陷阱需要避免:
va_arg宏的第2个参数不能被指定为char、short或者float类型。
因为char和short类型的参数会被转换为int类型,而float类型的参数会被转换为double类型 ……
例如,这样写肯定是不对的:
c = va_arg(ap,char);
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
c = va_arg(ap,int);
——《C陷阱与缺陷》p164
可能有人会问,VC中的三个宏不是已经实现了自动int对齐了吗? 如下:
下面是linux 2.6.22中的实现,其实是一样的意思
不过我想说的是:
① C标准对默认实际参数提升规则有明确规定。
也就是说, 带有可变长参数列表的函数, 绝对不会接受到char类型的实际参数。
② C标准对va_arg是否自动对齐没有任何说明。
也就是说自动对齐工作,编译器可做可不做。
在所有C实现上,能保证第①点,不能保证第②点,所以尽管编译器实现了自动对齐,也要按标准来。