函数除了可以接收固定的参数之外,还可以接受不确定的函数参数,称为变参函数,典型的变参函数是我们已经用得非常熟练的printf和scanf函数,它们都可以接受不确定个数的参数,它们的函数声明形式如下:
int printf(const char *format, ...); int scanf(const char *format, ...); |
可以发现上面两个函数的声明都有一个共同点,那就是都含有一个占位符...
。这个...
并不是参数,而是告诉编译器,该函数是变参函数,不管该函数使用时参数有多少个,都对其一一做压栈处理,这就实现了变参函数。
变参函数的实现与栈密切相关,栈是一种数据结构,是一种只能在一端进行插入和删除的特殊线性表(线性表可以暂时理解成数组)。栈的存储规律是先进后出(First In Last Out),先入栈的数据被压入栈底,最后入栈的数据在栈顶,需要读数据时则从栈顶开始弹出数据(最后一个数据被第一个弹出来)。
一个栈中,栈底是固定的,而栈顶是浮动的,对栈的插入和删除操作,不需要改变栈底的位置。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。在i386机器中,栈底位于高地址,栈顶位于低地址,压栈(PUSH)使得栈顶地址变小,弹栈(POP)(也可以称为退栈)使栈顶地址变大。
栈在程序的运行中有着举足轻重的作用。栈可以用来在函数调用时存储断点信息,做递归时要用到栈。最重要的是栈保存了一个函数调用时所需要的维护信息,通常称为堆栈帧,保存函数的返回地址和参数,以及函数的局部变量。
下面我们来分析变参函数参数的压栈过程。一般来说,函数参数的入栈顺序是从右向左的,意味着,最右边的参数最先入栈,位于高地址处,而最左边的参数最后入栈,位于低地址。下面通过一个具体的例子来看函数的压栈操作。
#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; } |
编译并运行以上程序,可以顺序实现打印后续参数的效果(上面的代码只在32位系统下能够运行成功,这里增加-m32参数,表示编译32位架构下的可执行程序,编译之前需要执行apt-get install gcc-multilib
以安装32位的库和运行环境):
root@DESKTOP-38B6GK1:~/C# gcc myprint.c -m32 -o myprint root@DESKTOP-38B6GK1:~/C# ./myprint 12 34 56 78 |
以上代码,首先在print函数中使用占位符...
,因此该函数在编译时被当成变参函数来处理,对该函数调用中的参数将一一进行压栈。上述代码定义了一个int型指针p,由于函数参数的压栈顺序是从右向左,参数的存储的地址由高地址到低地址,所以p = &n + 1
得到的是指向第一个可变参数的地址,接下来通过循环一一取出函数中的参数。
在使用变参函数时,...
前面至少要有一个普通的参数。必须知道参数什么时候结束,如果没有给出变参函数的个数,直接结出第一个参数,则必须约定一个参数作为结束标志。
当然,C语言标准库也提供了用于实现变参函数的宏va_list, va_start, va_arg, va_end
,用于简化变参函数的操作,位于头文件stdarg.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) |
下面通过这些宏来重写print变参函数:
#include <stdarg.h> void myprint(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; } |
这里要注意的一点是,函数的压栈是按照4字节对齐的,小于4字节的统统按4字节对齐来入栈,而这里的_INTSIZEOF宏,就是用于实现内存中的字节对齐操作。宏va_start(ap, v)的作用是先得到变量v的地址,然后将其转化成char型指针,再加上v对齐之后所占用的内存大小,使指针指向下一个参数。注意此时的指针为char类型,所以接下来在使用va_arg(ap, t)时要将其强制转换为当前参数类型t的指针。对于宏va_arg(ap, t)要注意的是,“ap += _INTSIZEOF(t)”得到的是下一个参数的地址,再减去_INTSIZEOF(t)得到当前参数的地址。通过一个for循环就可以一一取出其中压栈的所有参数。最后一个宏va_end(ap)的作用清除指针,表示在接下来的部分不再使用该指针变量。
//通过变参函数的宏实现一个类似printf()的函数 void myprintf(const char *fmt, ...) { va_list p; char c; va_start(p, fmt); while (*fmt != '\0') { c = *fmt; if (c != '%') { putchar(c); } else { fmt++; switch (*fmt) { case 'd': printf("%d", *((int *)p)); va_arg(p, int); break; case 'c': printf("%c", *((int *)p)); va_arg(p, int); break; case 'f': printf("%f", *((double *)p)); va_arg(p, double); break; } } fmt++; } // end while va_end(p); return; } |