1、C 语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf 就是使用的变长参数接口,在感受到 printf 强大的魅力的同时,是否想挖据一下到底printf 是如何实现的呢?这里我们一起来挖掘一下 C 语言变长参数的奥秘。先考虑这样一个问题:如果我们不使用 C 标准库(libc) 中提供的 Facilities,我们自己是否可以实现拥有变长参数的函数呢?我们不妨试试。一步一步进入正题,我们先看看固定参数列表函数,void fixed_args_func(int a, double b, char *c)printf(“a = 0x%pn“, printf(“b
2、 = 0x%pn“, printf(“c = 0x%pn“, 对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过 通过 通过return 0;a = 0x0022FF50b = 0x0022FF54c = 0x0022FF5C从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。我们基本可以得出这样一个结论:c.addr = b.addr + x_sizeof(b); /*注意: x_sizeof != sizeof,后话再说 */b.addr =
3、a.addr + x_sizeof(a);有了以上的“等式 “,我们似乎可以推导出 void var_args_func(const char * fmt, . ) 函数中,可变参数的位置了。起码第一个可变参数的位置应该是:first_vararg.addr = fmt.addr + x_sizeof(fmt); 根据这一结论我们试着实现一个支持可变参数的函数:void var_args_func(const char * fmt, . ) char *ap;ap = (char*)printf(“%dn“, *(int*)ap); ap = ap + sizeof(int);printf(“
4、%dn“, *(int*)ap);ap = ap + sizeof(int);printf(“%sn“, *(char*)ap);int main()var_args_func(“%d %d %sn“, 4, 5, “hello world“);输出结果:45hello worldvar_args_func 只是为了演示,并未根据 fmt 消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了,如果你把这个程序拿到 solaris 9 下,运行后,一定得不到正确的结果,为什么呢,后续再说。先来解释一下这个程序。我们用 ap 获取第一个变参的地址,我们知道第一个变参是 4,一个 int
5、 型,所以我们用(int*)ap 以告诉编译器,以 ap 为首地址的那块内存我们要将之视为一个整型来使用,*(int*)ap 获得该参数的值;接下来的变参是5,又一个 int 型,其地址是 ap + sizeof(第一个变参),也就是 ap + sizeof(int),同样我们使用*(int*)ap 获得该参数的值;最后的一个参数是一个字符串,也就是 char*,与前两个 int型参数不同的是,经过 ap + sizeof(int)后,ap 指向栈上一个 char*类型的内存块(我们暂且称之 tmp_ptr, char *tmp_ptr)的首地址,即 ap - printf(“%sn“, ap
6、)是意图将 ap 所指的内存块作为字符串输出了,但是 ap - 前面说过,如果将 var_args_func 放到 solaris 上,一定是得不到正确结果的?为什么呢?由于内存对齐。编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的。上述例子中,我是根据反编译后的汇编码得到的参数间隔,还好都是 4,然后在代码中写死了。为了满足代码的可移植性, C 标准库在 stdarg.h 中提供了诸多 Facilities 以供实现变长长度参数时使用。这里也列出一个简单的例子,看看利用标准库是如何支持变长参数的:#i
7、nclude void std_vararg_func(const char *fmt, . ) va_list ap;va_start(ap, fmt);printf(“%dn“, va_arg(ap, int);printf(“%fn“, va_arg(ap, double);printf(“%sn“, va_arg(ap, char*);va_end(ap);int main() std_vararg_func(“%d %f %sn“, 4, 5.4, “hello world“);输出:45.400000hello world对比一下 std_vararg_func 和 var_arg
8、s_func 的实现,va_list 似乎就是 char*, va_start 似乎就是 (char*)/* 这个函数用来格式化带参数的字符串 */int vspf(char *fmt, .) va_list argptr; /声明一个转换参数的变量int cnt; va_start(argptr, fmt); /初始化变量 cnt = vsnprintf(buffer,bufsize ,fmt, argptr);/将带参数的字符串按照参数列表格式化到 buffer 中va_end(argptr); /结束变量列表,和 va_start 成对使用 return(cnt); int main(i
9、nt argc, char* argv)int inumber = 30; float fnumber = 90.0; char string4 = “abc“; vspf(“%d %f %s“, inumber, fnumber, string);printf(“%sn“, buffer); return 0;下面我们来探讨如何写一个简单的可变参数的 C 函数.写可变参数的 C 函数要在程序中用到以下这些宏 : 使用可变参数应该有以下步骤: 1)首先在函数里定义一个 va_list 型的变量,这里是 arg_ptr,这个变量是指向参数的指针. 2)然后用 va_start 宏初始化变量 ar
10、g_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数. 3)然后用 va_arg 返回可变的参数,并赋值给整数 j. va_arg 的第二个参数是你要返回的参数的类型,这里是 int 型. 4)最后用 va_end 宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用 va_arg 获取各个参数. 如果我们用下面三种方法调用的话,都是合法的,但结果却不一样: 可变参数在编译器中的处理我们知道 va_start,va_arg,va_end 是在 stdarg.h 中被定义成宏的,由于:1)硬件平台的不同 2)编译器的不同Micro
11、soft Visual StudioVC98Includestdarg.h 中,typedef char * va_list;/*把 va_list 被定义成 char*,这是因为在我们目前所用的 PC 机上,字符指针类型可以用来存储内存单元地址。而在有的机器上 va_list 是被定义成 void*的*/#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) 使 ap 不再指向堆栈,而是跟 NULL 一样.有些直接定义为(void*)0),这样编译器不会为 va_end 产生代码,例如 gcc 在 linux 的 x86 平台就是这样定义的
12、. 在这里大家要注意一个问题: 由于参数的地址用于 va_start 宏,所以参数不能声明为寄存器变量或作为函数或数组类型. */这里有两个地方需要深入挖掘一下:1、#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) 首先 ap+=sizeof(int),已经指向下一个参数的地址了 .然后返回 ap-sizeof(int)的 int*指针,这正是第一个可变参数在堆栈里的地址(图 2).然后用*取得这个地址的内容 (参数值)赋给 j.高地址|-| |函数返回地址 | |-| |. | |-|-va_arg 后 ap 指向 |第 n 个参数
13、(第一个可变参数) | |-|-va_start 后 ap 指向 |第 n-1 个参数(最后一个固定参数 )| 低地址|-|- 使 ap 不再指向堆栈,而是跟NULL 一样.有些直接定义为(void*)0),这样编译器不会为 va_end 产生代码,例如 gcc 在 linux的 x86 平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于 va_start 宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于 va_start, va_arg, va_end 的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.可变参数在编程中要注意的
14、问题因为 va_start, va_arg, va_end 等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 有人会问:那么 printf 中不是实现了智能识别参数吗?那是因为函数 printf 是从固定参数 format 字符串来分析出参数的类型 ,再调用 va_arg 的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.如果simple_va_fun()改为: void simple_va_fun
15、(int i, .) va_list arg_ptr; char *s=NULL;va_start(arg_ptr, i); s=va_arg(arg_ptr, char*); va_end(arg_ptr); printf(“%d %sn“, i, s); return 0; 可变参数为 char*型,当我们忘记用两个参数来调用该函数时 ,就会出现 core dump(Unix) 或者页面非法的错误(window 平台 ).但也有可能不出错,但错误却是难以发现,不利于我们写出高质量的程序. 以下提一下 va 系列宏的兼容性. System V Unix 把 va_start 定义为只有一个参
16、数的宏: va_start(va_list arg_ptr); 而 ANSI C 则定义为 : va_start(va_list arg_ptr, prev_param); 如果我们要用 system V 的定义,应该用 vararg.h 头文件中所定义的宏,ANSI C 的宏跟 system V 的宏是不兼容的,我们一般都用 ANSI C,所以用 ANSI C 的定义就够了,也便于程序的移植.小结: 可变参数的函数原理其实很简单,而 va 系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的 C 函数时, 有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在 C+里,我们应该
17、利用 C+的多态性来实现可变参数的功能,尽量避免用 C 语言的方式来实现.printf 研究下面是一个简单的 printf 函数的实现:#include “stdio.h“#include “stdlib.h“void myprintf(char* fmt, .) /一个简单的类似于 printf 的实现,/ 参数必须都是 int 类型char* pArg = NULL; /等价于原来的 va_listchar c;pArg = (char*) /注意不要写成 p = fmt !因为这里要对 /参数取址,而不是取值pArg += sizeof(fmt); /等价于原来的 va_startdoc
18、 =*fmt;if (c != %)putchar(c); /照原样输出字符else/按格式字符输出数据switch(*+fmt)case d:printf(“%d“,*(int*)pArg);break;case x:printf(“%#x“,*(int*)pArg);break;default:break;pArg += sizeof(int); /等价于原来的 va_arg+fmt;while (*fmt != 0);pArg = NULL; /等价于 va_endreturn;int main(int argc, char* argv)int i = 1234;int j = 5678;myprintf(“the first test:i=%d“,i,j);myprintf(“the secend test:i=%d; %x;j=%d;“,i,0xabcd,j);system(“pause“);return 0;在 intel+win2k+vc6 的机器执行结果如下:the first test:i=1234the secend test:i=1234; 0xabcd;j=5678;