zoukankan      html  css  js  c++  java
  • 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参数的版本,否则就非常麻烦了。
  • 相关阅读:
    win10安装tomcat7
    分布式任务调度平台XXL-Job搭建
    定时任务
    分散读取与聚集写入
    通道(Channel)的原理获取
    直接缓冲区和非缓冲区
    摘:"error LNK2019: 无法解析的外部符号 该符号在函数 中被引用" 错误原因
    摘:static,const,inline,define的意义
    摘:LIB和DLL的区别与在VC中的使用
    VS2010 DLL库生成和使用
  • 原文地址:https://www.cnblogs.com/super119/p/2011349.html
Copyright © 2011-2022 走看看