C语言中的可变参数宏/函数,及可变参数在函数中的传递问题全解析

做ScheduleDownload,要做一个logger,这个logger的大致结构如下:

Code: Select all
#ifdef _DEBUG
#define LOGGER(log_level, filename, line, format, ...)   \
   logger_action(log_level, filename, line, format, __VA_ARGS__);
#else
#define LOGGER
#endif

/*
* Log the strings. filename should be __FILE__ and line should be __LINE__
*/
void logger_action(LOG_LEVEL log_level, LPCTSTR filename, int line, LPCTSTR format, ...);

#define UTILS_RVIF_WITH_LOG(expr, val, log_level, filename, line, format, ...)   \
   if (!(expr)) {   \
      LOGGER(log_level, filename, line, format, __VA_ARGS__);   \
      return val;   \
   }


这里很清楚了,真正的函数是logger_action,两个宏分别包装了一下。这里:

1. 在宏定义中,使用__VA_ARGS__来表示可变参数,前面用...即可。如果可变参数为空,那么,理论上就会多产生一个逗号导致编译失败 (format参数后面多一个逗号),此时,__VA_ARGS__会自动消除多余的逗号。这是VC编译器的动作,如果是GNU的编译器,要这样 写##__VA_ARGS__,通过##来将多余的逗号去掉(##一般用来连接字符串的,但是在这里就有去掉前面多余逗号的作用)。 __VA_ARGS__是C99规范中规定出来的关键字,在VC中,要在Visual Studio 2005开始支持。

2. __VA_ARGS__不能出现在函数实现中,只能出现在宏里面。所以这就带来一个问题:在logger_action中,我们其实不是想自己分析 format和后面的可变参数,我们仅仅想把这些都传递给StringCchPrintf而已,于是尝试在logger_action中这样处理:

Code: Select all
    // handle format & args
   va_list args;
    va_start(args, format);
   UTILS_RETURN_IF_FAIL(SUCCEEDED(StringCchPrintf(log_str_buf + log_str_cur_index, _countof(log_str_buf) - log_str_cur_index, format, args)));
   va_end(args);


va_list就是一个char *,va_start是一个宏,它的作用就是将args这个参数设置成format参数地址+format参数的字节数 -- 说白一些就是,将args设置成函数栈中format以后的位置上,这样args就指向了可变参数的开头。接着可以使用va_arg参数将可变参数一个一 个取出,这也是为什么va_arg宏要提供一个参数type的原因:va_arg根据参数type来决定往后取多少字节出来。最后的va_end就是将 args设成NULL。

所以va_list/va_start/va_arg/va_end其实非常简单,就是指针操作,将不确定的参数从函数堆栈中取出。这里我们只需要让StringCchPrintf来处理即可,于是我天真的将args参数传递给了StringCchPrintf。

结 果是:编译不出错,执行出错,StringCchPrintf生成的字符串是一堆乱七八糟的东西。开始Debug,通过观察函数的栈,传入的可变参数是 OK的,证明__VA_ARGS__在一堆宏之间传递没有问题。那为什么StringCchPrintf取不出这些可变参数呢?其实非常简单:

就 像前面说的一样,具有可变参数的函数在处理时,使用的是va_list/va_start...这些宏,这些宏是在本函数的堆栈上进行指针操作,而我们在 调用StringCchPrintf的时候,可变参数部分传入的是args,前面也说了,args其实类型是char *,就是一个地址,根本代表不了那一堆可变参数。StringCchPrintf能取出的唯一参数就是args,里面的值是logger_action函 数format参数之后的堆栈地址!!自然出错了,没crash就不错了。

OK,那应该怎么做呢?结论是:
1. 在logger_action函数中,将可变参数一个一个取出,用汇编将这些参数一个一个的压入StringCchPrintf函数的栈中。这种做法可移植性很差,不同编译器和不同平台上运行都有可能出问题,因为牵扯到汇编。
2. 其实我们相当于在做一个mysprintf,里面调用sprintf。除非用方法1,否则是无法实现的。幸运的是,sprintf有个兄弟叫 vsprintf,这个带v的函数最后不是接收...的参数,而是接受一个va_list类型的参数,也就是说,vsprintf和sprintf不同的 是,它不是在自己的堆栈上找可变参数,而是在我们给定的va_list参数地址上找可变参数。Great!于是查找StringCchPrintf有没有 这样一个兄弟 -- 有!StringCchVPrintf。于是代码只需要修改一个字符就OK了:

Code: Select all
    // handle format & args
   va_list args;
    va_start(args, format);
   UTILS_RETURN_IF_FAIL(SUCCEEDED(StringCchVPrintf(log_str_buf + log_str_cur_index, _countof(log_str_buf) - log_str_cur_index, format, args)));
   va_end(args);


这样就OK了!StringCchVPrintf会在args参数指定的地址开始,根据format中的定义,找寻对应的参数。测试通过,程序工作正常。

总结,主要是两点:
1. __VA_ARGS__不能出现在函数中,只能在宏中使用
2. 要将可变参数在函数中传递,要看被传入的函数有没有一个va_list参数的版本,否则就非常麻烦了。
posted @ 2011-04-10 14:43  super119  阅读(2058)  评论(0编辑  收藏  举报