1. 简介
可否想过 C 语言中最常见常用的 printf() 函数是如何做到接收任意类型、任意数量的参数的呢?
实际上,printf() 中通过可变参数特性来接收任意类型、任意数量的参数。可变参数通过占位 ...
显式指示,例如以下函数的形参就是可变参数:
void fun (...)
{
// do something
}
2. 相关 API
在函数中怎么使用这些可变参数呢?C 语言在头文件 <stdarg.h> 中提供了三个操作可变参数的 API:va_start
、va_arg
、va_end
,前缀 va 即 variable argument(可变参数)。
这三个 API 都为函数宏,功能如下:
- va_start
va_start
的声明如下(伪代码):
void va_start (va_list ap, last);
ap
为可变参数列表对象,需要在调用 va_start
前创建。va_list
内包含一个成员指针,即 ap
内有指向可变参数的指针。
last
为可变参数的占位符前的形参。一般情况下,使用可变参数的函数需要提供一个形参用于函数调用时指定可变参数的个数,否则在程序中将无法得知具体的可变参数个数。
va_start
用于初始化可变参数列表对象,把 ap
内的可变参数指针指向 last
的下一个参数即可变参数列表的第一个参数。
- va_arg
va_arg
的声明如下(伪代码):
type va_arg (va_list ap, type);
type
为可变参数的数据类型。
va_arg
用于获取 ap
的可变参数参数指针所指的参数并作为返回值。获取后该指针将根据 type
偏移一定字节数,指向下一个可变参数。
需要注意的是,使用 GCC 编译时,当可变参数类型为 char 或 short 即少于四个字节,可变参数将占据四个字节大小。因此,此时的函数实参 type
应显式指示为 int。
- va_end
va_end
的声明如下(伪代码):
void va_end (va_list ap);
va_end
用于释放可变参数列表对象,需要在函数最后调用完成收尾工作。如果在释放后继续操作可变参数列表对象,结果是未知的。
3. 应用方法
前文提到,一般情况下,使用可变参数的函数需要提供一个形参用于函数调用时指定可变参数的个数。
以以下求和函数为例:
int sum(int n, ...)
{
int sum = 0;
va_list valist;
va_start(valist, n);
while (n--)
{
sum += va_arg(valist, int);
}
va_end(valist);
return sum;
}
调用 sum() 函数时,指定可变参数的个数 n
。在函数中,可以调用 n 次 va_arg
获取所有传递的可变参数。
但对于 printf() 而言,调用函数时并没有指定 n
,那么它是怎样得知可变参数个数的呢?实际上,printf() 内会根据给定字符串中格式为 %<...>
的占位符推算出可变参数的个数。
现可实现简单的 printf() 函数:
int MyPrint(const char* s, ...)
{
if (s == NULL)
{
return -1;
}
va_list valist;
va_start(valist, s);
while (*s)
{
if (*s == '%')
{
s++;
switch (*s)
{
case 'c':
{
putchar(va_arg(valist, int));
break;
}
case 's':
{
const char* p = va_arg(valist, char*);
while (*p)
{
putchar(*p++);
}
break;
}
define :
{
va_end(valist);
return -1;
}
}
s++;
}
else
{
putchar(*s);
s++;
}
}
va_end(valist);
return 0;
}
在程序中使用 MyPrint():
int main(int argc, char **argv)
{
MyPrint("Hello World!\n");
char* s = "123456";
MyPrint("%s%s\n", s, "789");
}
编译后,运行结果如下:
kong@ubuntu:/mnt/hgfs/share/pj_cpp$ output/app
Hello World!
123456789
kong@ubuntu:/mnt/hgfs/share/pj_cpp$
4. 实现原理
可变参数的实现与编译器强相关,不同编译器的实现方式很可能不一样。例如有的 32 位编译器中可变参数都存放在栈上,而有的 64 位编译器则将部分可变参数存放到寄存器上。
更详细的介绍有可见:揭密X86架构C可变参数函数实现原理