欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C语言中变参函数的实现细节

程序员文章站 2022-04-27 19:16:08
C语言的函数虽然不具备C++的多态性,但也可以接受参数不确定的情况,当然,C语言中的变参函数实际在功能上是受限的,废话不多讲,下面来看看变参函数的边边角角的问题。 讨论之前我们来...

C语言的函数虽然不具备C++的多态性,但也可以接受参数不确定的情况,当然,C语言中的变参函数实际在功能上是受限的,废话不多讲,下面来看看变参函数的边边角角的问题。

讨论之前我们来看一下最熟悉的变参函数printf的原型声明:

--------------------------------------------------------------------------------

int printf(const char *format, ...);

--------------------------------------------------------------------------------

注意到,在函数中声明其参数是可变的方法是三个点“...”,但同时,这个函数必须要有一个固定的参数,比如printf里面的这个format,也就是说变参函数的参数数目至少是一个。这是由C语言中实现变参的原理---计算堆栈地址---决定的。顺着printf函数我们来看看它的定义是什么:

--------------------------------------------------------------------------------

int __printf(const char *format, ...)

{

va_list arg;

int done;

va_start(arg, format);

done = vfprintf(stdout, format, arg);

va_end(arg);

return done;

}

--------------------------------------------------------------------------------

(注意到库函数中内部定义的变量和函数用了双下划线开头,这也是我们写应用程序时尽量不要用双下划线开头的原因,我们也不应该使用单下划线开头的函数和变量,因为那也是系统保留的)

其中发现__printf函数里用了va_list,va_start,va_end等宏,事实上,在__printf中调用的vfpirntf函数还用到了一个叫做va_arg的宏,这几个宏就是编写变参函数的关键。现在我们自己写一个最简单的变参函数,先来个感性认识:

--------------------------------------------------------------------------------

#include

#include

void simple_va_fun(int i, ...)

{

va_list arg_ptr; //定义一个用来指向函数变参列表的指针arg_ptr

int j;

va_start(arg_ptr, i); //使arg_ptr指向第一个可变参数

j = va_arg(arg_ptr, int); //取得arg_ptr当前所指向的参数的值,并使arg_ptr指向下一个参数

va_end(arg_ptr); //指示提取参数结束

printf("%d %d\n", i, j);

return;

}

int main(void)

{

simple_va_fun(3, 4);

return 0;

}

--------------------------------------------------------------------------------

如代码中的注释所示,arg_ptr实际上是一个指向函数变参列表的指针,va_list实际上是void指针类型。

va_start用来初始化这个指针,使之指向变参列表中的第一个参数,注意到它的第二个参数是变参函数的那个固定参数。

va_arg利用已经初始化了的arg_ptr指针来取得变参列表中各个参数的值,第一个参数是变参列表指针,第二个参数是当前参数的类型。

va_end宏用来提示结束参数结束,在LINUX的glibc实现中,va_end实际上就是一个空语句(void)0

各个宏定义在头文件stdarg.h中声明,因此我们需要包含这个头文件。其具体的定义如下:

--------------------------------------------------------------------------------

#define _AUPBND (sizeof(acpi_native_int) - 1)

#define _ADNBND (sizeof(acpi_native_int) - 1)

#define _bnd(X, bnd) (((sizeof(X)) + (bnd)) & (~(bnd)))

#define va_start(ap, A) (void)((ap) = (((char *)&(A)) + (_bnd(A, _AUPBND)))

#defind va_arg(ap, T) (*(T*)(((ap) += (_bnd(T, _AUPBND))) - (_bnd(T, _ADNBDN))))

#define va_end(ap) (void)0

--------------------------------------------------------------------------------

这些宏定义都比较繁琐,主要目的是为了适应不同系统的地址对齐问题。

上面说过,va_start的功能实际上是使ap指针指向第一个变参,A就是我们的第一个固定参数,不考虑地址对齐,最简单的办法当然如下:

ap = &A + sizeof(A)

上述代码其实也是实现的这个简单的功能,但经过宏_AUPBND和_bnd之后,就能保证ap指向的地址至少是关于acpi_native_int对齐的,打个比方,如果此时A的地址是0x0003,而且A的类型占用4个字节,而当前系统要求4个字节对齐,那么就让_AUPBND中的sizeof参数为4,经过多次宏替代之后ap的地址值就会是0x0008,而简单地用上面的算式ap = &A + sizeof(A)计算出的结果是0x0007。

同样地,va_arg宏替代在不考虑任何移植性问题时,要取得当前变参的值并使指针指向下一个参数最简单的办法如下:

*((ap+=sizeof(T)) - sizeof(T))

这个需要稍微解释一下,首先,C里面的参数压栈是从右到左顺序压栈的,因此可以想象,第一个固定参数在栈顶(LINUX进程映像中栈是倒着增长的,这个地址是所有参数中最小的),第二个参数(也就是第一个变参)在紧接着固定参数之上,以此类推。因此,要想ap指针不断指向下一个参数,就必须让它每次都加上当前指向的变量所占内存的大小即 ap+=sizeof(T) 的含义。

接下来,利用这个地址值又减去sizeof(T),实际上地址值又回到上一个参数处(注意,此时ap指针的值并未改变,也就是说,va_arg宏实现获取第一个变参的值的时候是先使ap指向第二个变参,然后再去获取第一个变参的值),然后取值。

va_end宏就比较简单了,虽然各种平台的实现细节不一样,但是道理都是一样的,在glibc中va_end被简单地实现为一个空语句。

由此可见,实际上C语言的所谓变参函数是很笨的,它基本上啥智能都没有,不能跟C++的多态性和符号重载相比,我们在传递参数的时候虽然可以传递不定个数的参数,但是这些参数都必须在函数实现中给予一一处理。所以我还是比较推崇C++呵呵!

至于printf这个调皮鬼,上面看到它的原型了,里面还调用了vfprintf函数,这个函数就不分析了(实在太长了),它里面就用了va_arg来获取各个变参的值。printf之所以可以识别各种变量类型,是因为你调用它的时候必须用printf修饰符,也就是%d,%f,%s等等来指定你的参数,printf是很笨的,它是不知道的。