C语言可变长参数函数与默认参数提升

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 Promotion,此外,相应的实参如果是float型的也要被提升为double类型,这条规则称为Default Argument Promotion

3、可变长参数函数

熟悉C的人都知道,C语言支持可变长参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用(...)表示,比如我们常用的printf()\execl()函数等;printf函数的原型如下:

int printf(const char *format, ...);
   
   
   
   

注意,采用这种形式定义的可变参数函数,至少需要一个普通的形参,比如上面代码中的*format,后面的省略号是函数原型的一部分。

C语言定义了一系列宏来完成可变参数函数参数的读取和使用:宏va_startva_argva_end;在ANSI C标准下,这些宏定义在stdarg.h中。三个宏的原型如下:


   
   
   
   
  1. void va_start(va_list ap, last); // 取第一个可变参数(如上述printf中的i)的指针给ap,
  2. // last是函数声明中的最后一个固定参数(比如printf函数原型中的*fromat);
  3. type va_arg(va_list ap, type); // 返回当前ap指向的可变参数的值,然后ap指向下一个可变参数;
  4. // type表示当前可变参数的类型(支持的类型位int和double);
  5. void va_end(va_list ap); // 将ap置为NULL

当一个函数被定义为可变参数函数时,其函数体内首先要定义一个va_list的结构体类型,这里沿用原型中的名字,ap。va_start使ap指向第一个可选参数。va_arg返回参数列中的当前参数并使ap指向参数列表中的下一个参数。va_end把ap指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。

下面是一个具体的示例(摘自wikipedia):


   
   
   
   
  1. #include
  2. double average(int count, ...)
  3. {
  4. va_list ap;
  5. int j;
  6. double tot = 0;
  7. va_start(ap, count); //使va_list指向起始的参数
  8. for(j= 0; j
  9. tot+=va_arg(ap, double); //检索参数,必须按需要指定类型
  10. va_end(ap); //释放va_list
  11. return tot/count;
  12. }

 

4、默认参数提升在可变参数函数的陷阱

如果明白了C语言的可变参数函数,让我们实现一个简易的my_printf
1. 它只返回void, 不记录输出的字符数目
2. 它只接受"%d"按整数输出、"%c"按字符输出、"%%"输出'%'本身
很多人的答案如下:


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

很可惜,这样的代码是错误的!

简单的说,我们用va_arg(ap,type)取出一个参数的时候,
type绝对不能为以下类型:
——charsigned char、unsigned char
——short、unsigned short
——signed shortshort 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
——charshort和相应的signedunsigned类型的实际参数提升到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);

 

再来看一个具体的例子吧:


   
   
   
   
  1. #include
  2. #include
  3. void read_args_from_va_good(int i, ...)
  4. {
  5. va_list arg_ptr;
  6. va_start(arg_ptr, i);
  7. /* This is right. */
  8. printf( "%c\n", va_arg(arg_ptr, int));
  9. printf( "%d\n", va_arg(arg_ptr, int));
  10. printf( "%f\n", va_arg(arg_ptr, double));
  11. va_end(arg_ptr);
  12. }
  13. void read_args_from_va_bad(int i, ...)
  14. {
  15. va_list arg_ptr;
  16. va_start(arg_ptr, i);
  17. /* This is wrong. */
  18. printf( "%c\n", va_arg(arg_ptr, char));
  19. printf( "%d\n", va_arg(arg_ptr, short));
  20. printf( "%f\n", va_arg(arg_ptr, float));
  21. va_end(arg_ptr);
  22. }
  23. int main()
  24. {
  25. char c = 'c';
  26. short s = 0;
  27. float f = 1.1f;
  28. read_args_from_va_good( 0, c, s, f);
  29. read_args_from_va_bad( 0, c, s, f);
  30. return 0;
  31. }

上面的代码用gcc4.4.0编译,会有警告:


   
   
   
   
  1. va_arg.c: In function ‘read_args_from_va_bad’:
  2. va_arg.c: 47: warning: ‘ char’ is promoted to ‘ int’ when passed through ‘...’
  3. va_arg.c: 47: note: (so you should pass ‘ intnotchar’ to ‘va_arg’)
  4. va_arg.c: 47: note: if this code is reached, the program will abort
  5. va_arg.c: 48: warning: ‘ short int’ is promoted to ‘ int’ when passed through ‘...’
  6. va_arg.c: 48: note: if this code is reached, the program will abort
  7. va_arg.c: 49: warning: ‘ float’ is promoted to ‘ double’ when passed through ‘...’
  8. va_arg.c: 49: note: if this code is reached, the program will abort

 

运行gcc4.4.6生成的程序时,运行到第23行时,输出Illegal instruction,程序退出。查看了一下gcc4.4.6生成的汇编代码,发现没有为read_args_from_va_bad()生成有效的代码。


   
   
   
   
  1. astrol@astrol:~/c$ gdb va_arg -q
  2. Reading symbols from /home/astrol/c/va_arg...done.
  3. (gdb) run
  4. Starting program: /home/astrol/c/va_arg
  5. c 0 1.100000
  6. c
  7. 0
  8. 1.100000
  9. Program received signal SIGILL, Illegal instruction.
  10. 0x08048452 in read_args_from_va_bad (i=0) at va_arg.c:44
  11. 44 va_start(arg_ptr, i);
  12. (gdb) x/i $pc
  13. => 0x8048452 12>: ud2
  14. (gdb)

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对齐了吗? 如下:


   
   
   
   
  1. #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
  2. #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
  3. #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
  4. #define va_end(ap) ( ap = (va_list)0 )

下面是linux 2.6.22中的实现,其实是一样的意思


   
   
   
   
  1. #define _AUPBND (sizeof (acpi_native_int) - 1)
  2. #define _ADNBND (sizeof (acpi_native_int) - 1)
  3. /*
  4. * Variable argument list macro definitions
  5. */
  6. #define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
  7. #define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
  8. #define va_end(ap) (void) 0
  9. #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

不过我想说的是:

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

    C标准对va_arg是否自动对齐没有任何说明

也就是说自动对齐工作,编译器可做可不做。

在所有C实现上,能保证第点,不能保证第点,所以尽管编译器实现了自动对齐,也要按标准来。


参考链接:

http://www.cppblog.com/ownwaterloo/archive/2009/04/21/unacceptable_type_in_va_arg.html

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

http://www.spongeliu.com/331.html

(C语言可变参数函数取参方法)

http://wildpointer.net/2012/04/01/argument_promotion/

(参数类型提升)

http://www.spongeliu.com/325.html

(弱类型?C语言参数提升带来的一个陷阱)

----------------------------------------------------------------------------------------------------------------------------------------

补充(2015-9-19 12:24:07): 关于参数默认提升过程中的符号扩展问题

今天浏览博文时,看到《C语言中的类型提升——基础概念,但还有很多人搞不清 》这篇博文,觉得有必要说下符号扩展这个问题。

首先,什么是符号扩展? 符号扩展的定义:简单来说,符号扩展是一个整数从位数较小扩展到位数较多的过程。

需要澄清的一点是:符号扩展问题和默认参数提升这两个概念本身是没有半毛钱关系的,只是在实现默认参数提升这个标准的过程中会牵扯到符号扩展问题。也就是说,符号扩展问题并不仅仅在是在默认参数提升过程中存在,它还存在于其它很多情况下,比如不同类型的数据之间进行转换时也会进行符号扩展。

在看具体实例之前,我们先来具体看看符号扩展的规则是怎样的,看下图:

C语言可变长参数函数与默认参数提升_第1张图片

总结如下:

  1. 对于无符号整数,很简单,只需要在高位补0就可以了,即零扩展
  2. 对于有符号整数,需要区分正数和负数:① 对于正数,规则和无符号整数一样,零扩展。 ② 对于负数,高位补1就可以了。对于有符号整数的规则又称符号位扩展
你可能会问为什么是这样的规则,简单来说就是有符号整数在计算机中的表示方法是二进制补码,那么最高位就是符号位,符号位扩展就是为了保留整数的符号不变。

参考链接:
http://www.cnblogs.com/mydomain/archive/2012/11/20/2780017.html
http://blog.sina.com.cn/s/blog_6c97f3a301017gim.html (关于C语言中的符号扩展)
http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=750663
http://blog.csdn.net/kangroger/article/details/23246713
http://blog.csdn.net/cow__sky/article/details/22475441?utm_source=tuicool
http://www.cnblogs.com/del/archive/2010/04/15/1712702.html
https://graphics.stanford.edu/~seander/bithacks.html
http://www.94cto.com/index/Article/content/id/59973.html
http://blog.jobbole.com/73095/
http://www.jizhuomi.com/software/303.html
http://blog.csdn.net/thinkinwm/article/details/8627109
http://blog.csdn.net/andy205214/article/details/4975636
----------------------------------------------------------------------------------------------------------------------------------------

关于想了解变长参数列表函数原理的童鞋,我这里不想多说了,需要强调的一点是:只要理解基本的函数栈帧概念就能很好地理解这一切了。我给几个链接如下,说的很好,还有配图,实在是完美,给需要的童鞋参考。

《从变长参数到格式化字符串1:谈变长参数》

《C语言的那些小秘密之变参函数的实现》

《可变参数函数详解》

《C语言中可变参数函数实现原理》

《可变参数》

《深入理解C语言的函数调用过程 》

你可能感兴趣的:(C)