7.2 变参函数的实现方法

此前介绍的自定义函数都是一些确定参数的函数,但是我们却使用了变参函数。例如,常用的printf()、scanf()函数等就是典型的变参函数。printf()函数和scanf()函数的声明形式如下:


printf(const char*,……);

scanf(const char*,……);


读者可以发现上面两个函数的声明中有一个共同点,那就是都含有一个占位符“……”。占位符在这里并不是参数,只是告诉编译器,该函数是变参函数,不管该函数使用时的参数有多少,都对其一一做压栈处理,这就实现了变参函数。

说到压栈,读者应该并不陌生,在第1章讲解堆栈的概念时特地分析了函数的压栈操作,但是并没有提及函数参数的压栈是如何进行的,接下来就介绍函数参数的压栈。其实,函数参数的压栈和函数中数组的压栈操作类似。在第1章讲解函数中数组的压栈操作时讲到,数组的压栈是从最后一个元素开始的,从高地址到低地址,数组中的第一个元素最后被压栈,而函数参数的压栈操作也是从右向左进行的,从最后一个参数开始压栈,直到将第一个参数压栈为止。为了加深读者的印象,下面通过一段代码来看具体的压栈操作。


include<stdio.h>

void print(int n,……)

{

int*p,i;

p=&n+1;

for(i=0;i<n;i++)

printf("%d\t",p[i]);

printf("\n");

return;

}

int main()

{

print(4,12,34,56,78);

return 0;

}


运行结果:


12 34 56 78


从上面的代码可以看到,首先在print()函数中使用了占位符“……”,因此该函数在编译的时候被当成变参函数来处理,对该函数调用中的参数一一进行压栈处理。如图7-2所示为函数参数在栈中的存储结构。上述代码定义了一个int型指针变量p,由于函数参数的压栈顺序是从右向左,由高地址到低地址,所以在函数中通过“p=&n+1;”得到的是第一个可变参数的地址,接下来通过一个for循环一一取出函数中的参数。在此使用第一个函数参数表示传递可变参数的个数。在使用变参函数的时候,必须知道参数什么时候结束。如果没有给出变参函数的个数,直接给出第一个参数,那么必须约定一个参数作为结束标志。

当然,在C语言中,系统也提供了实现变参函数的宏,接下来就通过下面一段代码来了解如何采用系统提供的宏来实现变参函数。


include<stdio.h>

include<stdarg.h>

void print(int n,……)

{

int arg,i;

va_list p;

va_start(p,n);

for(i=0;i<n;i++)

{

arg=va_arg(p,int);

printf("%d\t",arg);

}

printf("\n");

va_end(p);

return;

}

int main()

{

print(3,21,32,54);

return 0;

}


7.2 变参函数的实现方法 - 图1

图 7-2 函数参数在栈中的存储结构

运行结果:


21 32 54


在讲解变参函数的宏的实现方法之前,先来看系统实现变参函数的代码。


typedef char*va_list;

define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))

define va_start(ap,v)(ap=(va_list)&v+_INTSIZEOF(v))

define va_arg(ap,t)((t)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))

define va_end(ap)(ap=(va_list)0)


由于va_list等价于char*,因此在print()函数中定义的p就是一个字符型指针变量。接下来看宏_INTSIZEOF(n)的定义,这主要是为了实现内存中的字节对齐操作。宏va_start(ap,v)的作用是先得到变量v的地址,然后将其转换为char型指针,再加上变量v所占用的内存大小,使指针ap指向下一个参数,注意此时的指针为char类型的指针,所以接下来在使用宏va_arg(ap,t)的时候要将其强制转换为此时参数的类型t的指针。对于宏va_arg(ap,t)要注意的是,“ap+=_INTSIZEOF(t)”得到的是下一个参数的地址,再减去_INTSIZEOF(t)得到的当前参数的地址。通过一个for循环就可以一一取出其中压栈的所有参数,最后一个宏“#define va_end(ap)”的作用是清除ap指针,表明在接下来的部分不再使用该指针变量。

了解了系统实现变参函数的方法,就可以通过宏定义来实现变参函数了,看下面的代码。


include<stdio.h>

typedef char*va_list;

define va_start(ap,v)(ap=(va_list)&v+sizeof(v))

define va_arg(ap,t)((t)((ap+=sizeof(t))-sizeof(t)))

define va_end(ap)(ap=(va_list)0)

void print(int n,……)

{

int arg,i;

va_list p;

va_start(p,n);

for(i=0;i<n;i++)

{

arg=va_arg(p,int);

printf("%d\t",arg);

}

printf("\n");

va_end(p);

return;

}

int main()

{

print(4,12,34,56,78);

return 0;

}


运行结果:


12 34 56 78


上面的实现方式不再调用系统提供的头文件,而是将其实现的宏提取出来,同时去掉了字节对齐操作,同样成功地实现了变参函数。但是这样会存在一个问题,因为函数的压栈操作是4字节对齐,所以如果这里的参数不是int型,而是char型,那么就会出现错误,如:


include<stdio.h>

typedef char*va_list;

define va_start(ap,v)(ap=(va_list)&v+sizeof(v))

define va_arg(ap,t)((t)((ap+=sizeof(t))-sizeof(t)))

define va_end(ap)(ap=(va_list)0)

void print(int n,……)

{

int arg,i;

va_list p;

va_start(p,n);

for(i=0;i<n;i++)

{

arg=va_arg(p,char);

printf("%c\t",arg);

}

printf("\n");

va_end(p);

return;

}

int main()

{

print(4,'A','B','C','D');

return 0;

}


运行结果:


A


这时打印出来的只有字符“A”,后面的字符没有能够成功地打印出来,我们可以通过图7-3来加以分析。看看参数的压栈操作,先通过“va_start(p,n);”使p指向函数的第一个参数,其中,p的值为&n+4,但是接下来使用“arg=va_arg(p,char);”取参数时就出现了问题,由于char型变量占用内存大小为1字节,而压栈操作采用的是4字节对齐,因此通过for循环就不可能取出后面的字符。

7.2 变参函数的实现方法 - 图2

图 7-3 函数参数采用4字节对齐在栈中的存储结构

为了成功实现所有字符的打印操作,应该在代码中加上字节对齐的操作。下面的代码用于实现一个类似于printf()的函数。


include<stdio.h>

include<stdlib.h>

typedef char*va_list;

define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))

define va_start(ap,v)(ap=(va_list)&v+_INTSIZEOF(v))

define va_arg(ap,t)((t)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))

define va_end(ap)(ap=(va_list)0)

void myprintf(char*fmt,……)

{

va_list p;

char c;

va_start(p,fmt);

do

{

c=*fmt;

if(c!='%')

{

putchar(c);

}

else

{

switch(*++fmt)

{

case'd':

printf("%d",((int)p));

break;

case'c':

printf("%c",((int)p));

break;

case'f':

printf("%3.2f",((double)p));

va_arg(p,int);

default:

break;

}

va_arg(p,int);

}

++fmt;

}while(*fmt!='\0');

va_end(p);

return;

}

void main()

{

int a=12;

short b=56;

char c='A';

double f=123.2;

myprintf("a=%d\t b=%d\t c=%c\t f=%f\n",a,b,c,f);

return;

}


运行结果:


a=12 b=56 c=A f=123.20


从上面的代码可以看出,实现了字节对齐操作后,不管参数为何种类型,均可以成功地打印出来。对于变参函数的实现需要注意的就是参数的压栈采用的是4字节对齐。对于以上代码中的实现方法,有几个地方需要注意:如果参数是double类型,那么就应该多执行一次“va_arg(p,int);”,因为double类型在内存中占用的内存大小为8字节;如果参数是float类型,压栈的时候采用的也是8字节对齐,如果采用它本身所占用的内存大小进行操作,打印出来的就是错误的结果。