转:https://www.cnblogs.com/bettercoder/p/3488299.html
在ANSI C中,这些宏的定义位于stdarg.h中,典型的实现如下:
typedef char * va_list;
va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数前面的参数):
#define va_start(list,param1) ( list = (va_list)¶m1+ sizeof(param1) )
va_arg宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型):
#define va_arg(list,mode) ( (mode *) ( list += sizeof(mode) ) )[-1]
va_end宏,清空va_list可变参数列表:
#define va_end(list) ( list = (va_list)0 )
注:以上sizeof()只是为了说明工作原理,实际实现中,增加的字节数需保证为为int的整数倍
如:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
为了理解这些宏的作用,我们必须先搞清楚:C语言中函数参数的内存布局。首先,函数参数是存储在栈中的,函数参数从右往左依次入栈。
以下面函数为讨论对象:
void test(char *para1,char *param2,char *param3, char *param4)
{
va_list list;
......
return;
}
在linux中,栈由高地址往低地址生长,调用test函数时,其参数入栈情况如下:
注:list和param1之间,应该还有eip,ebp.这里是为了讲解方便吧。
当调用va_start(list,param1) 时:list指针指向情况对应下图:
最复杂的宏是va_arg。它必须返回一个由va_list所指向的恰当的类型的数值,同时递增va_list,使它指向参数列表中的下一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标,所以va_arg宏首先使用sizeof来确定需要递增的大小,然后把它直接加到va_list上,这样得到的指针再被转换为要求的类型。因为该指针现在指向的位置"过"了一个类型单位的大小,所以我们使用了下标-1来存取正确的返回参数。
下面是实际用例:
#include <stdio.h>
#include <stdarg.h>
void var_test(char *format, ...)
{
va_list list;
va_start(list,format);
char *ch;
while(1)
{
ch = va_arg(list, char *);
if(strcmp(ch,"") == 0)
{
printf("\n");
break;
}
printf("%s ",ch);
}
va_end(list);
}
int main()
{
var_test("test","this","is","a","test","");
return 0;
}
附:可变参数应用实例
1.printf实现
#include <stdarg.h>
int printf(char *format, ...)
{
va_list ap;
int n;
va_start(ap, format);
n = vprintf(format, ap);
va_end(ap);
return n;
}
2.定制错误打印函数error
#include <stdio.h>
#include <stdarg.h>
void error(char *format, ...)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, "Error: ");
vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
return;
}
可变长参数列表误区与陷阱——va_arg不可接受的类型
实现一个有可变长参数列表函数的时候,会使用到stdarg.h(这里不讨论varargs.h)中提供的宏。
例如,我们要实现一个简易的my_printf:
1. 它只返回void, 不记录输出的字符数目
2. 它只接受"%d"按整数输出、"%c"按字符输出、"%%"输出'%'本身
如下:
#include <stdarg.h>
void my_printf(const char* fmt, ... )
{
va_list ap;
va_start(ap,fmt); /* 用最后一个具有参数的类型的参数去初始化ap */
for (;*fmt;++fmt)
{
/* 如果不是控制字符 */
if (*fmt!='%')
{
putchar(*fmt); /* 直接输出 */
continue;
}
/* 如果是控制字符,查看下一字符 */
++fmt;
if ('\0'==*fmt) /* 如果是结束符 */
{
assert(0); /* 这是一个错误 */
break;
}
switch (*fmt)
{
case '%': /* 连续2个'%'输出1个'%' */
putchar('%');
break;
case 'd': /* 按照int输出 */
{
/* 下一个参数是int,取出 */
int i = va_arg(ap,int);
printf("%d",i);
}
break;
case 'c': /* 按照字符输出 */
{
/** 但是,下一个参数是char吗*/
/* 可以这样取出吗? */
char c = va_arg(ap,char);
printf("%c",c);
}
break;
}
}
va_end(ap); /* 释放ap—— 必须! 见相关链接*/
}
这与《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行,应该改为:
printf("%c",c);
同理, 如果需要使用short和float, 也应该这样:
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”。
见: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