Windows 下 C/C++ 可变参数宏实现技巧
在开发过程中,有很多阶段,每个阶段可能会注重不同的重点,我们可能会在不同阶段让程序输出或者打印不同的信息以反应运行的情况,所以我们必须分阶段的使得程序输出我们在每个阶段所要关心的信息,甚至在最后让程序不再输出信息。这就要用到了宏定义!
我们知道,在 linux 下很方便的就能实现可变参数宏的定义,
比如:
#define myprint(fmt, a...) printf("%s,%s(),%d:" fmt "/n", __FILE__,__FUNCTION__,__LINE__, ##a) 就定义了自己的输出宏,当不必再输出这些可能是调式,跟踪,断言,日志 ... 的信息时,可以再定义宏为空:
#define myprintf(fmt,a...)
这样,重新编译后,这些宏引用的地方将全部没有语句,从而省去这些开销。
但是,在 windows 下,一般我们采用的 VC6.0,VS2003,VS2005,VS2008( 待定 ) 编辑器中自带的 C/C++ 编译器并不支持变参宏的定义, gcc 编译器支持,据说最新版本的 C99 也支持。
可以在windows下这样定义宏:
#define myprint printf
但是,当后期不想 再要宏输出了,只能定义 #define myprint为空,在那些有宏调用的代码区会留下类似 ("DEBUG:>> %d,%s,%f",idx,"weide001",99.001);这样的语句,它应该会被程序运算一次,应该会像函数参数那样被压栈,出栈一次,从而 增加了程序的运行开销,不是一个上策。
所以,在 windows 下需要变通一下,以下四种方式可以作为今后 windows 下定义可变参宏定义的参考:
( 1 )引用系统变参函数:
# include < stdarg. h>
# ifdef _WIN32
# define vsnprinf _vsnprintf
# define vsprinf _vsprintf
# endif
int my_print(const char* file ,cosnt char* fun ,const char* line , const char * fmt, . . . )
{
int t = 0;
char out[ 1024]="" ;
va_list ap;
/*
Test Env:
gcc version 3.4.5 20051201 (Red Hat 3.4.5-2)
Result:
使用 vsprintf 时:
当 fmt 的长度大于 1024 时出现 : 段错误
当 fmt 的长度小于 1024 时出现 : 正常
使用 vsnprintf 时:
当 fmt 的长度大于 1024 时出现 : 多余的字符不保存到 out 中
当 fmt 的长度小于 1024 时出现 : 正常
vsnprintf 的返回值同 snprintf 很相似
---------------------------------------------------
Test Env:
Microsoft Windows XP [ 版本 5.1.2600]
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8168 for 80x86
Result:
使用 _vsprintf 时:
当 fmt 的长度大于 1024 时出现 : 段错误
当 fmt 的长度小于 1024 时出现 : 正常
使用 _vsnprintf 时:
当 fmt 的长度大于 1024 时出现 : 多余的字符不保存到 out 中
当 fmt 的长度小于 1024 时出现 : 正常
_vsnprintf 的返回值同 _snprintf 很相似
*/
va_start ( ap, fmt) ;
t = vsnprintf( out, 1024, fmt, ap) ;
va_end ( ap) ;
printf ( "%s,%s(),%d: %s/n" , file,fun,line,out) ;
return ( t) ;
}
#define myprint(str) my_print(__FILE__,__FUNCTION__,__LINE__,str)
只能输出一个字符串参数。啥也别说了,这个肯定很烂,这里主要是记住这个变参函数的实现方式。
( 2 )新的 C99 规范支持了可变参数的宏,具体使用如下:
#include <stdarg.h>
#define myprint(fmt, ...) printf(fmt,__VA_ARGS__)
( 3 )这个很雷人: ( 用 ‘_’ 来代替 ‘,’ ,否则报错或者警告实参太多或者实参个数不一致 )
#define _ ,
#define mysprintf(geter, fmt, args) sprintf(geter, fmt, args)
{
char str[128] = "";
mysprintf(str, "name=%s age=%d weight=%f" _ "weide001" _ 23 _ 57.9);// 调用很累,和以往的方式出入太大
}
( 4 )使用 ## :
#define mysprintf(geter) sprintf##geter
{
char str[128] = "";
mysprintf( (str,"name=%s age=%d weight=%f","weide001",23,57.9) );// 也有点雷人,调用时多出来了一层括弧
}
可变参数的宏里的 ## 作用
GCC 始终支持复杂的宏,它使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。例如下面的例子:
#define debug(format, args...) fprintf (stderr, format, args)
但是在 debug 可变参数为 0 的时候, debug("hello /n") ,编译会出错,采用这样的方式:
#define debug(format, ...) fprintf (stderr, format, ##args) 就可以
## 的用法,文中是这样解释的:“这里,假如可变参数被忽略或为空,‘ ## ’操作将使预处理器( preprocessor )去除掉它前面的那个逗号。””这句话不明白,是 ## 的新功能,还是原有连接的功能的应用?
其实, ## 是粘连符
比如 windows 里 #define __T ( text ) L##text
就是在 text 前面加上了一个 L
__T("abc") 就成了 L"abc"
可变参数及可变参数宏的使用
我们在 C 语言编程中会遇到一些参数个数可变的函数 , 例如 printf() 这个函数 , 这里将介绍可变函数的写法以及原理 .
* 1. 可变参数的宏
一般在调试打印 Debug 信息的时候 , 需要可变参数的宏 . 从 C99 开始可以使编译器标准支持可变参数宏 (variadic macros), 另外 GCC 也支持可变参数宏 , 但是两种在细节上可能存在区别 .
1. __VA_ARGS__
__VA_ARGS__ 将 "..." 传递给宏 . 如
#define debug(format, ...) fprintf(stderr, fmt, __VA_ARGS__)
在 GCC 中也支持这类表示 , 但是在 G++ 中不支持这个表示 .
2. GCC 的复杂宏
GCC 使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。
#define debug(format, args...) fprintf (stderr, format, args)
这和上面举的那个定义的宏例子是完全一样的,但是这么写可读性更强并且更容易进行描述。
3. ##__VA_ARGS__
上面两个定义的宏 , 如果出现 debug("A Message") 的时候 , 由于宏展开后有个多余的逗号 , 所以将导致编译错误 . 为了解决这个问题, CPP 使用一个特殊的 ‘##’ 操作。
#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)
这里,如果可变参数被忽略或为空, ‘##’ 操作将使预处理器( preprocessor )去除掉它前面的那个逗号。如果你在宏调用时,确实提供了一些可变参数, GNU CPP 也会工作正常,它会把这些可变参数放到逗号的后面。
4. 其他方法
一种流行的技巧是用一个单独的用括弧括起来的的 " 参数 " 定义和调用宏 , 参数在宏扩展的时候成为类似 printf() 那样的函数的整个参数列表。
#define DEBUG(args) (printf("DEBUG: "), printf(args))
* 2. 可变参数的函数
写可变参数的 C 函数要在程序中用到以下这些宏 :
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 中 . 下面我们写一个简单的可变参数的函数 , 该函数至少有一个整数参数 , 第二个参数也是整数 , 是可选的 . 函数只是打印这两个参数的值 .
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;
va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d/n", i, j);
return;
}
在程序中可以这样调用 :
simple_va_fun(100);
simple_va_fun(100,200);
从这个函数的实现可以看到 , 使用可变参数应该有以下步骤 :
1) 首先在函数里定义一个 va_list 型的变量 , 这里是 arg_ptr, 这个变量是指向参数的指针 .
2) 然后用 va_start 宏初始化变量 arg_ptr, 这个宏的第二个参数是第一个可变参数的前一个参数 , 是一个固定的参数 .
3) 然后用 va_arg 返回可变的参数 , 并赋值给整数 j. va_arg 的第二个参数是你要返回的参数的类型 , 这里是 int 型 .
4) 最后用 va_end 宏结束可变参数的获取 . 然后你就可以在函数里使用第二个参数了 . 如果函数有多个可变参数的 , 依次调用 va_arg 获取各个参数 .
如果我们用下面三种方法调用的话 , 都是合法的 , 但结果却不一样 :
1)simple_va_fun(100);
结果是 :100 -123456789( 会变的值 )
2)simple_va_fun(100,200);
结果是 :100 200
3)simple_va_fun(100,200,300);
结果是 :100 200
我们看到第一种调用有错误 , 第二种调用正确 , 第三种调用尽管结果正确 , 但和我们函数最初的设计有冲突 . 下面一节我们探讨出现这些结果的原因和可变参数在编译器中是如何处理的 .
* 3. 可变参数函数原理
va_start,va_arg,va_end 是在 stdarg.h 中被定义成宏的 , 由于硬件平台的不同 , 编译器的不同 , 所以定义的宏也有所不同 , 下面以 VC++ 中 stdarg.h 里 x86 平台的宏定义摘录如下 :
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 )
定义 _INTSIZEOF(n) 主要是为了内存对齐 ,C 语言的函数是从右向左压入堆栈的 ( 设数据进栈方向为从高地址向低地址发展 , 即首先压入的数据在高地址 ). 下图是函数的参数在堆栈中的分布位置 :
低地址 |-----------------------------|<-- &v
| 第 n-1 个参数 ( 最后一个固定参数 )|
|-----------------------------|<--va_start 后 ap 指向
| 第 n 个参数 ( 第一个可变参数 ) |
|-----------------------------|
|....... |
|-----------------------------|
| 函数返回地址 |
高地址 |-----------------------------|
1. va_list 被定义为 char *
2. va_start 将地址 ap 定义为 &v+_INTSIZEOF(v), 而 &v 是固定参数在堆栈的地址 , 所以 va_start(ap, v) 以后 ,ap 指向第一个可变参数在堆栈的地址
3. va_arg 取得类型 t 的可变参数值 , 以 int 型为例 ,va_arg 取 int 型的返回值 :
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
4. va_end 使 ap 不再指向堆栈 , 而是跟 NULL 一样 . 这样编译器不会为 va_end 产生代码 .
在不同的操作系统和硬件平台的定义有些不同 , 但原理却是相似的 .
* 4. 小结
对于可变参数的函数 , 因为 va_start, va_arg, va_end 等定义成宏 , 所以它显得很愚蠢 , 可变参数的类型和个数需要在该函数中由程序代码控制 ; 另外 , 编译器对可变参数的函数的原型检查不够严格 , 对编程查错不利 .
所以我们写一个可变函数的 C 函数时 , 有利也有弊 , 所以在不必要的场合 , 无需用到可变参数 . 如果在 C++ 里 , 我们应该利用 C++ 的多态性来实现可变参数的功能 , 尽量避免用 C 语言的方式来实现 .
* 5. 附一些代码
#define debug(format, ...) fprintf(stderr, fmt, __VA_ARGS__)
#define debug(format, args...) fprintf (stderr, format, args)
#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)
// 使用 va... 实现
void debug(const char *fmt, ...)
{
int nBuf;
char szBuffer[1024];
va_list args;
va_start(args, fmt);
nBuf = vsprintf(szBuffer, fmt, args) ;
assert(nBuf >= 0);
printf("QDOGC ERROR:%s/n",szBuffer);
va_end(args);
}