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