现在的位置: 首页 > 综合 > 正文

变参函数实现细节

2018年02月17日 ⁄ 综合 ⁄ 共 2655字 ⁄ 字号 评论关闭

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 <stdio.h>
#include <stdarg.h>

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是很笨的,它是不知道的。

抱歉!评论已关闭.