现在的位置: 首页 > 综合 > 正文

由于变参宏引发的思考

2018年03月18日 ⁄ 综合 ⁄ 共 4466字 ⁄ 字号 评论关闭

    之前学习c语言就知道了va_start,va_arg, va_end这三个宏,记得当时是为了自己实现printf函数,最近有空又细看了下其实现。

    32位(编译器)和64位(编译器)下进行一个实验,这个实验将揭示变参宏的本质,同时也提出一个32位与64位编译器差异的问题:

    首先看如下代码

     #include <stdio.h>
    void fun(int a, ...)
    {
        int *tmp = &a;
        int i = 0;
        for (; i < a; ++i) {
            ++tmp;
            printf("%d\t", *tmp);
        }
        printf("\n");
     }

     int main()
     {
         fun(4, 1, 2, 3, 4);
         return 0;
     }

    测试环境为WIN7,当采用不同编译器,分别打开Visual Studio 2008 Command Prompt(32 位), Vistual Studio 2008 x64 Win64 Command Prompt(64位)。

    源程序保存为1.c,分别用两种编译器输入命令cl 1.c,最终生成1.exe。

    最终输出结果为:

    1 2 3 4(32位编译器)

    0 1 0 2(64位编译器)(注:如果将循环中a改为8,则显示为 0 1 0 2 0 3 0 4,待会将解释其中意义)

    从上面实验可以知道32位编译器是以4字节进行入栈,而64位编译器将以8字节入栈。具体原因可从源代码中看出:

    查看vs中include中的stdarg.h发现如下一段话:

    #if !defined(_WIN32)

    #error ERROR: Only Win32 target supported!

    #endif

    起初以为此段话的意思为只支持32位机器或者说是32位编译器,后来测试后发现,其实不然,虽然64位机器函数调用时,由于其寄存器数目较多,可以不采用堆栈方式进行参数传递,但是后来查阅资料,只要是有变参宏的一定以堆栈进行传递,这样64位机器与32位机器便无差异了。

    以下一个实验来验证_WIN32的意义:

    #include <stdio.h>

    int main()
    {
        #ifdef _WIN32
    printf("define _WIN32\n");
        #endif

        #ifdef _WIN64
      printf("define _WIN64\n");
       #endif

        printf("over \n");
return 0;
    }

    将以上代码分别在32位,64位编译器中进行编译并运行,输出结果如下:

    32:   define _WIN32

             over

    64:   define _WIN32

             define _WIN64

              over

    根据以上估计,_WIN32、_WIN64表示支持的位数,即64位机器既能支持64位,也能兼容32位,故其定义了两个宏。

    以上解决了32位机器与64位机器的问题,还没有具体讲到变参宏的具体实现,接着查看vs头文件。

    #define va_start _crt_va_start
    #define va_arg _crt_va_arg
    #define va_end _crt_va_end

    

    #elif   defined(_M_IX86)
        #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
        #define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
        #define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
        #define _crt_va_end(ap)      ( ap = (va_list)0 )
    #elif defined(_M_IA64)

        #ifdef  __cplusplus
            extern void __cdecl __va_start(va_list*, ...);
            #define _crt_va_start(ap,v)  ( __va_start(&ap, _ADDRESSOF(v), _SLOTSIZEOF(v), \
                          _ADDRESSOF(v)) )
        #else
            #define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _SLOTSIZEOF(v) )
        #endif

        #define _crt_va_arg(ap,t)    (*(t *)((ap += _SLOTSIZEOF(t)+ _APALIGN(t,ap)) \

                                                     -_SLOTSIZEOF(t)))

        #define _crt_va_end(ap)      ( ap = (va_list)0 )

      #elif defined(_M_AMD64)

      ......

    #ifdef  __cplusplus
        #define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )
    #else
        #define _ADDRESSOF(v)   ( &(v) )
    #endif

    #if     defined(_M_IA64) && !defined(_M_CEE_PURE)
        #define _VA_ALIGN       8
        #define _SLOTSIZEOF(t)   ( (sizeof(t) + _VA_ALIGN - 1) & ~(_VA_ALIGN - 1) )
        #define _VA_STRUCT_ALIGN  16 
        #define _ALIGNOF(ap) ((((ap)+_VA_STRUCT_ALIGN - 1) & ~(_VA_STRUCT_ALIGN -1)) \
        - (ap))
       #define _APALIGN(t,ap)  (__alignof(t) > 8 ? _ALIGNOF((uintptr_t) ap) : 0)
    #else
        #define _SLOTSIZEOF(t)   (sizeof(t))
        #define _APALIGN(t,ap)  (__alignof(t))
    #endif

    以上代码有些多,取va_start进行研究:即研究_crt_va_start;

     重点看这个宏,是64位编译的宏#define _crt_va_start(ap,v)  ( __va_start(&ap, _ADDRESSOF(v), _SLOTSIZEOF(v), _ADDRESSOF(v)) ),主要关系到_ADDRESSOF和_SLOTSIZEOF。

    可以卡看到宏_ADDRESSOF只是取当前单元的地址,32位于64位除了指针长度的不同其余无太大差别;再看_SLOTSIZEOF,#if     defined(_M_IA64) && !defined(_M_CEE_PURE)情况下为:

    #define _VA_ALIGN       8
    #define _SLOTSIZEOF(t)   ( (sizeof(t) + _VA_ALIGN - 1) & ~(_VA_ALIGN - 1) )

   可以看到此单元至少为8字节对齐的,而另一种情况为#define _SLOTSIZEOF(t)   (sizeof(t)),以当前t的长度对齐,从这点就可以解释开头代码的结果了(本文开头那段代码只是int * 的++,只增加4字节,所以32位、64位编译器编译出的可执行程序发生上述差异)。

   以上说了这么多,只是解释了变参宏在32位于64位编译环境下的不同现象,以及原因(虽然可能这些对于程序员来说是透明的),再来说说其具体实现,就只说说32位编译吧,这些baidu中比比皆是,就简略写写吧:

    typedef char *  va_list;

    #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
    #define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
    #define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    #define _crt_va_end(ap)      ( ap = (va_list)0 )

    首先_INTSIZEOF为字节对齐,可以看到字节将以int进行对齐,即4字节对齐,va_start将ap指针指向v以后的那个单元,va_arg将ap指针指向下一个单元,并返回原先单元所表示的值,va_end则将ap指针清零。

    还有一点小疑惑,希望各位知道问题所在的不吝赐教:

    void demo(int begin, ...)
   {

        //1

        va_list arg;

        //2

        va_start(arg, begin);

        int i = 0;
        for (; i < begin; ++i) {
        printf("address: %p, %d\n", arg, va_arg(arg, int));
        }
        va_end(arg);
    }

    int main()
    {
        demo(4, 1, 2, 3, 4);
        return 0;
    }

    即只要用了变参宏,之后再定义变量,必定报错,不管32位还是64位编译器,如上代码,只要将i的定义放到1或者2处将能正常编译通过。

    另外,之前曾baidu到一个demo,觉得有些问题,以下,是我修正后的代码,自测能用

    #include <stdio.h>
    #include <string.h>
    #include <stdarg.h>

    int demo(char msg, ...)
    {
        va_list arg;
        int argNo = 0;
        char *para;
        va_start(arg, msg);
        while(1) {
                para = va_arg(arg, char*);
                if (strcmp(para, "") == 0) {
                    break;
                }
                printf("parameter #%d is: %s\n", argNo, para);
                ++argNo;
         }
        va_end(arg);
        return 0;
    }

    int main() {
        demo('D', "This", "is", "a", "demo!", "");
        return 0;
    }

抱歉!评论已关闭.