zoukankan      html  css  js  c++  java
  • C函数和宏中的可变参数

    一:调用惯例

             函数的调用方和被调用方对函数如何调用应该有统一的理解,否则函数就无法正确调用。比如foo(int n, int m),调用方如果认为压栈顺序是m,n,而foo认为压栈顺序是n, m,那么这个函数就不会调用成功。

     

             因此,函数的调用方和被调用方对于函数如何调用需要有个明确的约定,双方都遵守同样的约定,函数才能调用成功,这种约定称为调用惯例,一个调用惯例一般会规定如下几个方面的内容:

             1:函数参数的传递顺序和方式

             函数参数的传递有多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己在从栈中将参数取出。如果有多个参数,调用惯例要规定函数调用方参数压栈的顺序:从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。

             2:栈的维护方式

             在函数将参数压栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,使得栈在函数调用前后保持一致。这个弹出的工作可以由函数调用方完成,也可以由函数本身完成。

             3:名字修饰策略

             不同的调用惯例对函数名有不同的修饰策略

     

             在C中,存在多个调用惯例,默认的调用惯例是cdecl,任何一个没有显示执行调用惯例的函数默认都是cdecl惯例。另外,_cdecl是非标准关键字,在不同的编译器中可以有不同的写法,比如在GCC中,使用:__attribute__((cdecl))。cdecl的调用惯例:参数从右至左的顺序入栈,参数出栈由调用方完成。

             除了cdecl调用惯例之外,还存在许多别的调用惯例,比如stdcall,fastcall等,不再赘述。

     

    二:函数中的可变参数

             printf函数的原型如下:

    int printf(const char *format, ...);

             printf函数就是可变参数的典范。除了第一个参数类型为const char *之外,可以追加任意数量,任意类型的参数。

             可变参数的实现,得益于C语言默认的cdecl调用惯例,它从右向左进行参数的入栈,比如函数:int  sum(unsigned  num,  ...);num表示后面会传递num个整数,当调用sum时:int n = sum(3, 16, 38, 53);参数在栈上的布局如下图:

             函数内部,可以使用num得到数字3,而且其他参数在栈上的排列就是在num的高地址方向,从而可以通过num的地址计算出其他参数的地址,所以,sum函数的实现如下:

    int sum(unsigned num, ...)
    {
        int *p = &num + 1;
        int ret = 0;
        while(num--)
            ret += *p++;
        
        return ret;
    }
    

             所以,cdecl调用惯例保证了参数的正确处理,但是在调用sum函数的时候,必须要知道有多少个不定参数,每个不定参数的类型是什么。因此printf在format中指定了参数类型和参数个数。

     

             可以使用stdarg.h中定义的宏来访问各个不定参数:

    #include <stdarg.h>
    
    void va_start(va_list ap, last);
    type va_arg(va_list ap, type);
    void va_end(va_list ap);
    void va_copy(va_list dest, va_list src);
    

           首先需要定义va_list类型的变量:va_list  ap;  该变量会依次指向各个可变参数。     va_list实际上是一个指针,指向各种不定参数,因类型不同,所以va_list以void *或char *为最佳选择。

     

             ap必须首先使用va_start初始化,ap只有经过va_start初始化之后,才可以被后续的va_arg和va_end使用。

             va_start(ap, last);其中last是函数最后一个具名参数,比如printf中的format。因va_start中会使用last的地址,因此该变量不能是寄存器变量,也不能是函数或者数组类型。

             经过va_start初始化之后,ap指向第一个可变参数。

            

             va_arg宏返回当前可变参数的值,并使ap指向下一个可变参数:type  va_arg(va_list ap,  type); type是ap指向的当前可变参数的类型。该宏还会修改ap,使其指向下一个可变参数。

             如果后续没有可变参数了,或者type与实际的可变参数类型不符,则va_arg会发生不可预知的错误。

     

             同一个函数中,每一个va_start必须跟着一个相应的va_end。经过va_end之后,ap将是未定义的。一般是将指针ap置NULL。

     

             这些宏可以如下实现:

    #define va_list             char *
    #define va_start(ap, arg)   (ap = (va_list)(&arg) + sizeof(arg))
    #define va_arg(ap, t)       (*(t*)((ap += sizeof(t)) – sizeof(t)))
    #define va_end(ap)          (ap = (va_list)0)
    

             va_copy将src复制到dest。行为上类似于,以同样的步骤和参数,将之前对src的调用,施加于dest上。

     

             va_start、va_arg、va_end和va_copy都是线程安全的。其中va_start、va_arg和va_end是C89中就定义的,在C99中,又新增了va_copy。

             下面的例子,是实现一个printf函数的简易版:

    #include <stdio.h>
    #include <stdarg.h>
    
    void foo(char *fmt, ...)
    {
       va_list ap;
       int d;
       char c, *s;
    
       va_start(ap, fmt);
       while (*fmt)
           switch (*fmt++) {
           case 's':              /* string */
               s = va_arg(ap, char *);
               printf("string %s
    ", s);
               break;
           case 'd':              /* int */
               d = va_arg(ap, int);
               printf("int %d
    ", d);
               break;
           case 'c':              /* char */
               /* need a cast here since va_arg only
                  takes fully promoted types */
               c = (char) va_arg(ap, int);
               printf("char %c
    ", c);
               break;
           }
       va_end(ap);
    }
    

    三:宏中的可变参数

             C99中,类似于函数,宏也可以接受可变参数。比如下面的例子:

    #define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)

             其中的”...”就是可变参数。在宏的调用中,它会展开为最后一个命名参数之后,’)’之前的所有token,包括逗号,并替换掉宏体中的”__VA_ARGS__”。

            

             比如下面的语句:

    debug("the int is %d, string is %s
    ", 3, "hello, world");

             经过宏替换之后,展开为下面的语句:

    fprintf (stderr, "the int is %d, string is %s
    ", 3, "hello, world");

             在GCC中,除了支持上面的写法之外,还支持使用更具描述性的名字表示可变参数,而不使用”__VA_ARGS__”。使用”argname...”的写法表示可变参数,其中argname是参数名,可以任意取。比如上面的宏定义,可以写成下面的形式,效果是一样的:

    #define debug(format, arg...)  fprintf(stderr, format, arg)

             但是,上面两种写法的debug宏还有一个共同的问题,就是必须提供一个可变参数,否则无法匹配宏,而且会出现语法错误,比如下面的语句:

    debug("hehe
    ");

             展开后,就会扩展为下面的语句:

    fprintf(stderr, "hehe
    ", );

             这显然会报语法错误。

     

             解决这个问题有两种方法:

             1:将所有参数都当成可变参数,将宏定义成下面的形式:

    #define debug(...)  fprintf (stderr, __VA_ARGS__)

    或是:

    #define debug(arg...)  fprintf(stderr, arghehe)

             2:使用”##”。当将”##”置于逗号和可变参数之间时,它有特殊的意义。比如下面的写法:

    #define debug(format, ...) fprintf (stderr, format, ##__VA_ARGS__)

    或是:

    #define debug(format, arg...)  fprintf(stderr, format, ##arg)

             这种情况下,当省略可变参数时,”##”会使得预编译器删除它前面的逗号。当确实提供了可变参数时,”##”不起作用,像正常的可变参数一样进行替换。

     

             推荐使用第二种方法。

     

    参考:

             《程序员的自我修养》

             https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html

             https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html

  • 相关阅读:
    LaTeX表格紧跟文字 (不影响下方文本对齐)
    latex减少图片和图片解释文字之间的距离、调整公式与正文间距离,调整空白大小:
    请收藏,Linux 运维必备的 40 个命令总结,收好了~
    将公式直接转化为Latex代码的神器-Mathpix Snip
    if __name__ == '__main__':的作用和原理【转】
    PyCharm,Terminal 常用快捷键
    Python包中 __init__.py文件的作用
    pycharm写代码光标变成了黑框
    客户端链接Blog
    Sublime 中文标题乱码
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247081.html
Copyright © 2011-2022 走看看