如果一定要说哪段C语言代码最“著名”,我想非“hello world”莫属了。大多数初学者人生中编写的第一段C语言代码就是这段“里程碑”式的代码:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
也正因为这段著名的程序,printf() 函数成为大多数C语言初学者接触到的第一个标准库函数。
C语言中的可变参数函数
随着学习的推进,初学者逐步学会调用别的C语言函数,以及定义自己的函数,观察力敏锐的会注意到 printf() 函数似乎与其他函数不太一样——printf()函数没有固定数目的参数,它似乎可以接收任意多的参数。
而其他C语言函数则不同,它们大都有固定数量的参数(0个,3个等),调用这些函数必须传递对应数目的参数。
有些持有“特殊论”的初学者认为像 printf() 这样的“可变参数”函数是“特殊的”,是系统定义的,我们程序员只能定义固定参数的函数,其实不是的,C语言是有手段定义自己“可变参数”函数的。
printf() 究竟是不是只能由系统定义的“特殊”函数呢?
怎样定义自己的可变参数函数?
事实上,标准库 <stdarg.h>
就是方便C语言程序员定义自己的“可变参数”函数的。如果读者和我一样使用的是 Linux 系统,则可以方便的通过 man 命令查询到相关库函数:
头文件<stdarg.h>
声明了 va_list 类型用于描述可变参数,并且定义了上述 4 个方法解析。这里不打算介绍过多枯燥的理论知识,我们直接看实例,请看相关C语言代码:
#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);
}
int main()
{
foo("%s, %d, %c\n", "hello", 12, 'm');
return 0;
}
上述代码定义了可变参数函数 foo(),它可以接收类似于 printf() 的函数,并且将 fmt 中的 s 解析为字符串,d 解析为整数,c 解析为字符,因此编译并执行这段C语言代码,可得到如下输出:
# gcc t.c
# ./a.out
string hello
int 12
char m
通过这段实例,可以看出使用C语言定义可变参数函数并不复杂,在处理可变参数时,只需先调用 va_start() 将参数序列加载到 va_list 结构的变量中,然后调用 va_arg() 依次解析。解析完毕后,再调用 va_end() 结束解析。
va_start -> va_arg -> va_end。
唯一需要注意的是使用 va_arg() 解析参数时,需要指定类型。但是这个过程也很简单,可变参数函数的实现者可以指定一套规则,用于约束函数调用者传递参数,这样就知道接下来需要解析的参数是何种类型。例如上面的C语言代码就约定了 fmt 中的 s 表示接下来的要解析的参数是字符串,d 表示整数等。
计算机是如何处理可变参数函数的?
C语言定义可变参数函数的过程并不复杂,借助于<stdarg.h>
,我们能够轻易的定义接收任意多参数的函数,不过到这里,有读者发现问题了:我们人类可以按照规则写出可变参数函数,但是计算机是如何理解这一套规则的呢?或者换句话说,计算机是如何处理“可变参数”的?
以Linux为例,看过我之前文章的读者应该明白,每个C语言程序进程都有属于自己的栈,进程中的每个函数则有属于自己的栈帧,当有函数调用时,例如:
foo("%d%d%d", 3,2,1);
C语言编译器会产生类似于下面这样的汇编代码:
push 1
push 2
push 3
push "%d%d%d"
call foo
也即将 foo() 函数的参数先压入栈中,然后再调用 foo() 函数。鉴于栈这种数据结构“先进后出”的特点,一般函数参数的入栈顺序是从右至左的。
[] // 空栈
-------------------------------
push 1: [1]
-------------------------------
push 2: [1]
[2]
-------------------------------
push 3: [1]
[2]
[3] // 参数 1 2 3 被压入栈中
-------------------------------
push "%d%d%d":[1]
[2]
[3]
["%d%d%d"]
-------------------------------
call foo ... // foo 函数开始使用参数
按照这样的参数入栈顺序,foo() 函数使用参数很方便,依次从栈中将参数取出就可以了。至于如何解析栈中的参数,则可以根据可变参数实现者指定的规则,例如在格式化字符串 fmt 中遇到 s 就解析为字符串等。
如果可变参数 foo() 接收到其他数目的参数,对于最终程序来说,也仅仅只需要修改压栈的参数数目,其他并无太多不同。
小结
本文主要讨论了C语言中可变参数函数的定义方法,以及计算机如何处理可变参数函数的过程,其实并不复杂。C语言不像C++那样支持函数重载,但是借助于可变参数函数和宏,我们可以像定义“伪类”那样,定义自己的“伪函数重载”,这是一种编程技巧,以后有机会再讨论了。