可变长参数列表误区与陷阱——va_arg不可接受的类型

实现一个有可变长参数列表函数的时候,会使用到stdarg.h(这里不讨论varargs.h)中提供的宏。


例如,我们要实现一个简易的my_printf:
1. 它只返回void, 不记录输出的字符数目
2. 它只接受"%d"按整数输出、"%c"按字符输出、"%%"输出'%'本身
如下:
 1  #include  < stdarg.h >
 2 
 3  void  my_printf( const   char *  fmt, ... )
 4  {
 5      va_list ap;
 6      va_start(ap,fmt);  /*  用最后一个具有参数的类型的参数去初始化ap  */
 7       for  (; * fmt; ++ fmt)
 8      {
 9           /*  如果不是控制字符  */
10           if  ( * fmt != ' % ' )
11          {
12              putchar( * fmt);  /*  直接输出  */
13               continue ;
14          }
15           /*  如果是控制字符,查看下一字符  */
16           ++ fmt;
17           if  ( ' \0 ' ==* fmt)  /*  如果是结束符  */
18          {
19              assert( 0 );   /*  这是一个错误  */
20               break ;
21          }
22           switch  ( * fmt)
23          {
24           case   ' % ' /*  连续2个'%'输出1个'%'  */
25              putchar( ' % ' );
26               break ;
27           case   ' d ' /*  按照int输出  */
28              {
29                   /*  下一个参数是int,取出  */
30                   int  i  =  va_arg(ap, int );
31                  printf( " %d " ,i);
32              }
33               break ;
34           case   ' c ' /*  按照字符输出  */
35              {
36                   /* 但是,下一个参数是char吗 */
37                   /*   可以这样取出吗?  */
38                   char  c  =  va_arg(ap, char );
39                  printf( " %c " ,c);
40              }
41               break ;
43          }
44      }
45      va_end(ap);   /*  释放ap—— 必须! 见相关链接 */
46  }


这与《C++程序设计语言》中的一道练习题很类似。
——需要支持"%c"控制符

在《C++程序设计语言-题解》中,给出了一个答案(中文p65页)。
但是, 如同上面的代码一样,它们都是 错误的!




简单的说,我们用 va_arg( ap, type)取出一个参数的时候,
type对不能为以下类型:
—— charsigned charunsigned char
—— shortunsigned short
—— signed shortshort intsigned short intunsigned short int
—— float


一个简单的理由是:
—— 调用者绝对不my_printf 传递以上类型的 实际参数


在C语言中,调用一个不带原型声明的函数时:
调用者会对 每个参数执行“默认实际参数 提升(default argument promotions)”。

同时,对可变长参数列表 超出最后一个类型声明的形式参数之后的 每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到double
——char、short和相应的signed、unsigned类型的实际参数提升到int
——如果int不能存储原值,则提升到unsigned int

然后,调用者将 提升后的参数 传递给被调用者。
所以,my_printf是 绝对无法接收到上述类型的实际参数的。



上面的代码的38与39行,应该改为:
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 );

这也是printf族函数没有用于short和float的控制符的原因。


附录:

在《C语言程序设计》对可变长参数列表的相关章节中,并没有提到这个陷阱。
但是有提到默认实际参数提升的规则:

在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double类型。
                ——《C语言程序设计》第2版  2.7 类型转换 p36



在其他一些书籍中,也有提到这个规则:


事情很清楚,如果一个参数没有声明,编译器就没有信息去对它执行标准的类型检查和转换。
在这种情况下,一个char或short将作为int传递,float将作为double传递。
这些做未必是程序员所期望的。
脚注:这些都是由C语言继承来的标准提升。
对于由省略号表示的参数,其实际参数在传递之前总执行这些提升(如果它们属于需要提升的类型),将提升后的值传递给有关的函数。——译者注
                ——《C++程序设计语言》第3版-特别版 7.6 p138

…… float类型的参数会自动转换为double类型,short或char类型的参数会自动转换为int类型 ……
                ——《C陷阱与缺陷》 4.4 形参、实参与返回值 p73


这里有一个陷阱需要避免:
va_arg宏的第2个参数不能被指定为 charshort或者 float类型。
因为char和short类型的参数会被转换为int类型,而float类型的参数会被转换为double类型 ……
例如,这样写 肯定是不对的:
c = va_arg(ap,char);
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
c = va_arg(ap,int);
                ——《C陷阱与缺陷》p164


2009/05/07 修改:
printf函数族有用于short的控制符“h”。
见: http://www.cplusplus.com/reference/clibrary/cstdio/printf/


相关链接:

——《可变长参数列表误区与陷阱——va_end是必须的吗?》
http://www.cppblog.com/ownwaterloo/archive/2009/04/21/is_va_end_necessary.html





作品采用 知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议进行许可。

转载请注明 :
文章作者 - OwnWaterloo
发表时间 - 2009年04月21日

原文链接 - http://www.cppblog.com/ownwaterloo/archive/2009/04/21/unacceptable_type_in_va_arg.html

 

Changelog:

1. C标准对默认实际参数提升规则有明确定。
也就是说, 带有可变长参数列表的函数, 绝对不会接受到char类型的实际参数。

2. C标准对va_arg是否自动对齐没有任何说明
你说的va_arg(va_list,type)是自动对齐, 只是在你的编译器上。
并不是所有编译器都能自动帮你完成这个工作。

在所有C实现上, 能保证第1点, 但不能保证第2点。
依赖于第2点, 代码就依赖于特定编译器。


你说va_arg(ap,type)是自动对齐, 证明你有研究过。
喜欢作这些研究的, 都是聪明的家伙。
但聪明的家伙总喜欢不按规矩办事


在gcc (GCC) 3.4.2 (mingw-special)中, type使用char, 会得到严重的警告:
`char' is promoted to `int' when passed through `...'
(so you should pass `int' not `char' to `va_arg')
note: if this code is reached, the program will abort
它会直接挂掉你的程序,来约束你必须按规矩办事。

你可能感兴趣的:(va_arg)