在 GNU C 中,宏可以接受可变数目的参数,就象函数一样,例如:
#define pr_debug(fmt,arg...) \
printk(KERN_DEBUG fmt,##arg)
用可变参数宏(variadic macros)传递可变参数表
你可能很熟悉在函数中使用可变参数表,如:
void
printf(const char* format, …);
直到最近,可变参数表还是只能应用在真正的函数中,不能使用在宏中。
C99编译器标准终于改变了这种局面,它允许你可以定义可变参数宏(variadic
macros),这样你就可以使用拥有可以变化的参数表的宏。可变参数宏就像下面这个样子:
#define
debug(…) printf(__VA_ARGS__)
缺省号代表一个可以变化的参数表。使用保留名 __VA_ARGS__
把参数传递给宏。当宏的调用展开时,实际的参数就传递给 printf()了。例如:
Debug(“Y
= %d\n”, y);
而处理器会把宏的调用替换成:
printf(“Y
= %d\n”, y);
因为debug()是一个可变参数宏,你能在每一次调用中传递不同数目的参数:
debug(“test”); //一个参数
可变参数宏不被ANSI/ISO C++ 所正式支持。因此,你应当检查你的编译器,看它是否支持这项技术。
用GCC和C99的可变参数宏,
更方便地打印调试信息
gcc的预处理提供的可变参数宏定义真是好用:
#ifdef DEBUG
#define dbgprint(format,args...) \
fprintf(stderr, format, ##args)
#else
#define dbgprint(format,args...)
#endif
如此定义之后,代码中就可以用dbgprint了,例如dbgprint("aaa%s",__FILE__);。感觉这个功能比较Cool:em11:
下面是C99的方法:
#define dgbmsg(fmt,...) \
printf(fmt,__VA_ARGS__)
新的C99规范支持了可变参数的宏
具体使用如下:
以下内容为程序代码:
#include
#include
#define LOGSTRINGS(fm, ...)
printf(fm,__VA_ARGS__)
int
main() { LOGSTRINGS("hello,
%d ",
10); return
0; }
但现在似乎只有gcc才支持。
可变参数的宏里的‘##’操作说明
带有可变参数的宏(Macros
with a Variable Number of Arguments)
在1999年版本的ISO
C标准中,宏可以象函数一样,定义时可以带有可变参数。宏的语法和函数的语法类似。下面有个例子:
#define debug(format, ...) fprintf (stderr, format,
__VA_ARGS__)
这里,‘…’指可变参数。这类宏在被调用时,它(这里指‘…’)被表示成零个或多个符号,包括里面的逗号,一直到到右括弧结束为止。当被调用时,在宏体(macro
body)中,那些符号序列集合将代替里面的__VA_ARGS__标识符。更多的信息可以参考CPP手册。
GCC始终支持复杂的宏,它使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。例如下面的例子:
#define
debug(format, args...) fprintf (stderr, format, args)
这和上面举的那个ISO
C定义的宏例子是完全一样的,但是这么写可读性更强并且更容易进行描述。
GNU
CPP还有两种更复杂的宏扩展,支持上面两种格式的定义格式。
在标准C里,你不能省略可变参数,但是你却可以给它传递一个空的参数。例如,下面的宏调用在ISO
C里是非法的,因为字符串后面没有逗号:
debug
("A message")
GNU
CPP在这种情况下可以让你完全的忽略可变参数。在上面的例子中,编译器仍然会有问题(complain),因为宏展开后,里面的字符串后面会有个多余的逗号。
为了解决这个问题,CPP使用一个特殊的‘##’操作。书写格式为:
#define
debug(format, ...) fprintf (stderr, format, ##
__VA_ARGS__)
这里,如果可变参数被忽略或为空,‘##’操作将使预处理器(preprocessor)去除掉它前面的那个逗号。如果你在宏调用时,确实提供了一些可变参数,GNU
CPP也会工作正常,它会把这些可变参数放到逗号的后面。象其它的pasted
macro参数一样,这些参数不是宏的扩展。
怎样写参数个数可变的宏
一种流行的技巧是用一个单独的用括弧括起来的的``参数"定义和调用宏,
参数在 宏扩展的时候成为类似printf()那样的函数的整个参数列表。
#define DEBUG(args) (printf("DEBUG: "), printf args)
if(n != 0) DEBUG(("n is %d\n", n));
明显的缺陷是调用者必须记住使用一对额外的括弧。
gcc 有一个扩展可以让函数式的宏接受可变个数的参数。 但这不是标准。另一种
可能的解决方案是根据参数个数使用多个宏 (DEBUG1, DEBUG2, 等等),
或者用 逗号玩个这样的花招:
#define DEBUG(args) (printf("DEBUG: "), printf(args))
#define _ ,
DEBUG("i = %d" _ i);
C99 引入了对参数个数可变的函数式宏的正式支持。在宏 ``原型" 的末尾加上符号 ...
(就像在参数可变的函数定义中),
宏定义中的伪宏 __VA_ARGS__ 就会在调用是
替换成可变参数。
最后,
你总是可以使用真实的函数, 接受明确定义的可变参数
如果你需要替换宏, 使用一个 函数和一个非函数式宏, 如#define
printf myprintf
本文在很大程度上改编自网友kevintz的“C语言中可变参数的用法”一文,在行文之前先向这位前辈表示真诚的敬意和感谢。
一、什么是可变参数
我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: int printf( const char* format, ...); 它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式: printf("%d",i); printf("%s",s); printf("the number is %d ,string is:%s", i,
s); 以上这些东西已为大家所熟悉。但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助.
二、写一个简单的可变参数的C函数
先看例子程序。该函数至少有一个整数参数,其后是占位符…,表示后面参数的个数不定.
在这个例子里,所有的输入参数必须都是整数,函数的功能是打印所有参数的值. 函数代码如下: //示例代码1:可变参数函数的使用
#include "stdio.h"
#include "stdarg.h"
voidsimple_va_fun(intstart, ...)
{
va_listarg_ptr;
intnArgValue =start;
intnArgCout=0;//可变参数的数目
va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
do
{
++nArgCout;
printf("the %d th arg: %d\n",nArgCout,nArgValue);//输出各参数的值
nArgValue = va_arg(arg_ptr,int);//得到下一个可变参数的值
} while(nArgValue != -1);
return;
}
intmain(intargc,char* argv[])
{
simple_va_fun(100,-1);
simple_va_fun(100,200,-1);
return0;
}
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤: ⑴在程序中将用到以下这些宏: void va_start( va_list arg_ptr, prev_param
); type va_arg( va_list arg_ptr, type ); void va_end( va_list arg_ptr ); va在这里是variable-argument(可变参数)的意思. 这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件. ⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。 ⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,也就是最后一个固定参数。 ⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。然后进行输出。 ⑸设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完下面这几个宏的内部实现机制后,自然就会明白。
三、可变参数在编译器中的处理
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同
2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装目录下的\vc98\include\stdarg.h)
typedef char * va_list; #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) &
~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v)
) #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) -
_INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 ) 下面我们解释这些代码的含义: 1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。
3、va_start的定义为&v+_INTSIZEOF(v),而&v是最后一个固定参数的起始地址,再加上其大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap,
v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。
这里要知道两个事情: ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。 (2)在VC等绝大多数C编译器中,参数进栈的顺序是由右向左的,因此,
参数进栈以后的内存模型如下图所示:最后一个固定参数的地址正好位于第一个可变参数之下,并且是连续存储的。 |—— —————————————| | 最后一个固定参数 | ->高内存地址处 |— ——————————————| ........................ |-------------------------------| | 第N个可变参数 |
->va_arg(arg_ptr,datatype)后arg_ptr所指的地方 |-------------------------------| ................... |——— ————————————| | 第一个可变参数 |
->va_start(arg_ptr,start)后arg_ptr所指的地方 | | 即第一个可变参数的地址 |——————————————— | |————————————— ——| | | | 最后一个固定参数 | -> start的起始地址 |—————————————— —| ............... |——————————————- | | | |——————————————— | -> 低内存地址处 (4)
va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了: #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) -
_INTSIZEOF(t)) ) 这个宏做了两个事情, ①用用户输入的类型对参数地址进行强制类型转换,得到用户所需要的值 ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。 (5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再
指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不
会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.
在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.
关于va_start, va_arg, va_end的描述就是这些了,我们要注意的
是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
四、可变参数在编程中要注意的问题
因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,
可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能 地识别不同参数的个数和类型.
有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数
printf是从固定参数format字符串来分析出参数的类型,再调用va_arg
的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 例如,在C的经典教材《the c
programming》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。
五、小结: 1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。 2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如 ⑴在固定参数中设标志—— printf函数就是用这个办法。后面也有例子。 ⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法——当可变参数的值为-1时,即认为得到参数列表的结尾。
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。这是一个不太方便 3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定: ①函数栈的生长方向 ②参数的入栈顺序 ③CPU的对齐方式 ④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现—将不再使用的指针设为NULL,这样可以防止以后的误操作。
4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信有经验的读者就可以写出适合于自己机器的实现来。下面就是一个例子
六、实践——自己实现简单的可变参数的函数。
下面是一个简单的printf函数的实现,参考了中的156页的例子,读者可以结合书上的代码与本文参照。
"font-size:16px;">#include"stdio.h"
#include "stdlib.h"
voidmyprintf(char* fmt, ...)//一个简单的类似于printf的实现,参数必须都是int 类型
{
char* pArg=NULL;//等价于原来的va_list
charc;
pArg = &fmt; //注意不要写成p = fmt !!因为这里要对参数取址,而不是取值
pArg += sizeof(fmt);//等价于原来的va_start
do
{
c =*fmt;
if(c !='%')
{
putchar(c); //照原样输出字符
}
else
{
//按格式字符输出数据
switch(*++fmt)
{
case'd':
printf("%d",*((int*)pArg));
break;
case'x':
printf("%#x",*((int*)pArg));
break;
default:
break;
}
pArg += sizeof(int);//等价于原来的va_arg
}
++fmt;
}while(*fmt !='\0');
pArg = NULL; //等价于va_end
return;
}
intmain(intargc,char* argv[])
{
inti = 1234;
intj = 5678;
myprintf("the first test:i=%d\n",i,j);
myprintf("the secend test:i=%d; %x;j=%d;\n",i,0xabcd,j);
system("pause");
return0;
}
在intel+win2k+vc6的机器执行结果如下:
the first test:i=1234 the secend test:i=1234; 0xabcd;j=5678;