实现一个有可变长参数列表函数的时候,会使用到stdarg.h(这里不讨论varargs.h)中提供的宏。
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绝对不能为以下类型: —— char、 signed char、 unsigned char —— short、 unsigned short —— signed short、 short int、 signed short int、 unsigned 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个参数不能被指定为 char、 short或者 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”。 见: 相关链接: ——《可变长参数列表误区与陷阱——va_end是必须的吗?》 本 作品采用 进行许可。 转载请注明 :
文章作者 - OwnWaterloo 发表时间 - 2009年04月21日原文链接 -
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 它会直接挂掉你的程序,来约束你必须按规矩办事。