void va_start(va_list pvar, name): 根据name参数获得可变参数的指针 pvar。 (type *) va_arg(va_list pvar, type): 获得下一个可变参数,需要指定参数的类型。 void va_end(va_list pvar): 结束使用可变参数,使它不再指向内存有效地址。
一、什么是可变参数
我们在 C 语言编程中有时会遇到一些参数个数可变的函数 , 例如 printf() 函数 , 其函数原型为 :
int printf( const char* format, ...);
它除了有一个参数 format 固定以外 , 后面跟的参数的个数和类型是可变的(用三个点 “…” 做参数占位符) , 实际调用时可以有以下的形式 : printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
以上这些东西已为大家所熟悉。但是究竟如何写可变参数的 C 函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。
long sum(int i,...) { int *p,j; long s = 0; p = &i+1; for (j=0;j<i;j++) s += p[j]; return s; } int main() { long Sum = sum(3,1,2,3); printf("%ld",Sum); return 0; } //Sum == 6
二、写一个简单的可变参数的 C 函数
先看例子程序。该函数至少有一个整数参数 , 其后占位符 … ,表示后面参数的个数不定 . 在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值 .
函数代码如下:
//示例代码1:可变参数函数的使用 #include "stdio.h" #include "stdarg.h" void simple_va_fun(int start, ...) { va_list arg_ptr; int nArgValue =start; int nArgCout=0; //可变参数的数目 va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。 do { ++nArgCout; printf("the %d th arg: %d",nArgCout,nArgValue); //输出各参数的值 nArgValue = va_arg(arg_ptr,int); //得到下一个可变参数的值 } while(nArgValue != -1); return; } int main(int argc, char* argv[]) { simple_va_fun(100,-1); simple_va_fun(100,200,-1); return 0; }
下面解释一下这些代码
从这个函数的实现可以看到 , 我们使用可变参数应该有以下步骤 :
⑴ 由于在程序中将用到以下这些宏 :
void va_start( va_list arg_ptr, prev_param ); type va_arg( va_list arg_ptr, type ); void va_end( va_list arg_ptr );
va 在这里是 variable-argument( 可变参数 ) 的意思 .
这些宏定义在 stdarg.h 中 , 所以用到可变参数的程序应该包含这个头文件 .
⑵ 函数里首先定义一个 va_list 型的变量 , 这里是 arg_ptr, 这个变
量是存储参数地址的指针 . 因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
⑶ 然后用 va_start 宏初始化 ⑵ 中定义的变量 arg_ptr, 这个宏的第二个参数是可变参数列表的前一个参数 , 即最后一个固定参数 .
⑷ 然后依次用 va_arg 宏使 arg_ptr 返回可变参数的地址 , 得到这个地址之后,结合参数的类型,就可以得到参数的值。
⑸ 设定结束条件,这里的条件就是判断参数值是否为 -1 。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完这几个宏的内部实现机制后,自然就会明白。
------------------------------------------------------------------------------- -----
( 二 ) 可变参数在编译器中的处理
我们知道 va_start,va_arg,va_end 是在 stdarg.h 中被定义成宏的 , 由于 1) 硬件平台的不同 2) 编译器的不同 , 所以定义的宏也有所不同 , 下面看一下 VC++6.0 中 stdarg.h 里的代码(文件的路径为 VC 安装目录下的 /vc98/include/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 )
下面我们解释这些代码的含义:
1 、首先把 va_list 被定义成 char* ,这是因为在我们目前所用的 PC 机上,字符指针类型可以用来存储内存单元地址。而在有的机器上 va_list 是被定义成 void* 的
2 、定义 _INTSIZEOF(n) 主要是为了某些需要内存的对齐的系统 . 这个宏的目的是为了得到最后一个固定参数的实际内存大 ?gt;> T谖业幕 魃现苯佑胹 izeof 运朔 创 妫 猿绦虻脑诵薪峁挂裁挥杏跋臁# ê 笪慕 吹轿易约旱氖迪郑 �?
3 、 va_start 的定义为 &v+_INTSIZEOF(v) , 这里 &v 是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行 va_start(ap, v) 以后 ,ap 指向第一个可变参数在的内存地址 , 有了这个地址,以后的事情就简单了。
这里要知道两个事情:
⑴ 在 intel+windows 的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2) 在 VC 等绝大多数 C 编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|——————————————————————————|
| 最后一个可变参数 | -> 高内存地址处
|——————————————————————————|
...................
|——————————————————————————|
| 第 N 个可变参数 | ->va_arg(arg_ptr,int) 后 arg_ptr 所指的地方 ,
| | 即第 N 个可变参数的地址。
|——————————————— |
………………………….
|——————————————————————————|
| 第一个可变参数 | ->va_start(arg_ptr,start) 后 arg_ptr 所指的地方
| | 即第一个可变参数的地址
|——————————————— |
|———————————————————————— ——|
| |
| 最后一个固定参数 | -> start 的起始地址
|—————————————— —| .................
|—————————————————————————— |
| |
|——————————————— | -> 低内存地址处
(4) va_arg(): 有了 va_start 的良好基础,我们取得了第一个可变参数的地址,在 va_arg() 里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到弦桓霾问 钠鹗嫉刂贰 ?
因此,现在再来看 va_arg() 的实现就应该心中有数了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
① 用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
② 计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。
(5)va_end 宏的解释: x86 平台定义为 ap=(char*)0; 使 ap 不再 指向堆栈 , 而是跟 NULL 一样 . 有些直接定义为 ((void*)0), 这样编译器不会为 va_end 产生代码 , 例如 gcc 在 linux 的 x86 平台就是这样定义的 . 在这里大家要注意一个问题 : 由于参数的地址用于 va_start 宏 , 所以参数不能声明为寄存器变量或作为函数或数组类型 . 关于 va_start, va_arg, va_end 的描述就是这些了 , 我们要注意的 是不同的操作系统和硬件平台的定义有些不同 , 但原理却是相似的 .
( 三 ) 可变参数在编程中要注意的问题
因为 va_start, va_arg, va_end 等定义成宏 , 所以它显得很愚蠢 , 可变参数的类型和个数完全在该函数中由程序代码控制 , 它并不能智能 地识别不同参数的个数和类型 . 有人会问 : 那么 printf 中不是实现了智能识别参数吗 ? 那是因为函数 printf 是从固定参数 format 字符串来分析出参数的类型 , 再调用 va_arg 的来获取可变参数的 . 也就是说 , 你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的 . 例如,在 C 的经典教材《 the c programming language 》的 7.3 节中就给出了一个 printf 的可能实现方式,由于篇幅原因这里不再叙述。
(四)小结 :
1 、标准 C 库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
2 、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
⑴ 在固定参数中设标志 —— printf 函数就是用这个办法。后面也有例子。
⑵ 在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法 .
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
3 、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
① 函数栈的生长方向
② 参数的入栈顺序
③ CPU 的对齐方式
④ 内存地址的表达方式
结合源代码,我们可以看出 va_list 的实现是由 ④ 决定的, _INTSIZEOF(n) 的引入则是由 ③ 决定的,他和 ①② 又一起决定了 va_start 的实现,最后 va_end 的存在则是良好编程风格的体现,将不再使用的指针设为 NULL, 这样可以防止以后的误操作。
4 、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。下面臼且桓隼 ?
(五)扩展 —— 自己实现简单的可变参数的函数。
下面是一个简单的 printf 函数的实现,参考了 <The C Programming Language> 中的 156 页的例子,读者可以结合书上的代码与本文参照。
#include "stdio.h" #include "stdlib.h" void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,//参数必须都是int 类型 { char* pArg=NULL; //等价于原来的va_list char c; pArg = (char*) &fmt; //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值 pArg += sizeof(fmt); //等价于原来的va_start do { c =*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_end return; } 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=1234
the secend test:i=1234; 0xabcd;j=5678;
#include <stdarg.h>//不定数目参数需要的宏 int max(int n,int num,...) { va_list x;//说明变量x va_start(x,num);//x被初始化为指向num后的第一个参数 int m=num; for(int i=1;i<n;i++) { //将变量x所指向的int类型的值赋给y,同时使x指向下一个参数 int y=va_arg(x,int); if(y>m)m=y; } va_end(x);//清除变量x return m; } main() { printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533)); }