C函数和宏中的可变参数

时间:2023-03-09 04:37:42
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);参数在栈上的布局如下图:

C函数和宏中的可变参数

函数内部,可以使用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