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\n", s);
           break;
       case 'd':              /* int */
           d = va_arg(ap, int);
           printf("int %d\n", 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\n", 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\n", 3, "hello, world");

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

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

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

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

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

debug("hehe\n");

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

fprintf(stderr, "hehe\n", );

         这显然会报语法错误。

 

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

         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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值