va_start、va_arg、va_end在参数可变函数中的使用

目录
[隐藏]

当无法列出传递函数的所有实参的类型和数目时,可用省略号指定参数表,在函数体中声明一个va_list,然后用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束。

C语言可变参数函数及三个宏va_start、va_arg和va_end的使用

一、可变参数函数的实例

大家熟知的printf()函数声明如下:

int  printf(const char * format, …);

它除了有一个参数format固定以外,后面跟的参数的个数和类型是
可变的,例如我们可以有以下不同的调用方法:

printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);

二、如何编写一个自已的可变参数函数

查了一下,在<stdarg.h>中定义了三个宏va_start()、va_arg()和va_end()用于实现可变参数。

  void va_start( va_list arg_ptr, prev_param );
  type va_arg( va_list arg_ptr, type );
  void va_end( va_list arg_ptr );

试编一个示例:

C++代码
  1. #include <stdio.h>  
  2. #include <stdarg.h>  
  3. #define ENDING_INT 0  
  4.   
  5. int SumAll(int number1,…)  //把参数加总  
  6. {  
  7.     va_list arg_pointer; //首先定义一个va_list型的变量,这个变量是指向参数的指针.  
  8.   
  9.     int current_number;  //当前的数字  
  10.     int total;           //数字之和  
  11.   
  12.     //用va_start初始化变量arg_pointer,这个宏的第二个参数是第一个可变参数(一个固定的参数)  
  13.     va_start(arg_pointer,number1);  
  14.     total=number1;  
  15.   
  16.     do  
  17.       {  
  18.         //用va_arg返回后续的可变参数, 类型是 int  
  19.         current_number=va_arg(arg_pointer,int);  
  20.         total += current_number;  
  21.       }  
  22.     while (current_number!=ENDING_INT);   //如果参数是结束标识(这里是ENDING_INT),则结束  
  23.     va_end(arg_pointer); //结束参数列表  
  24.   
  25.     return total;  
  26. }  
  27.   
  28. int main(int argc, char* argv[])  
  29. {  
  30.    int n;  
  31.   
  32.    n=SumAll(100,200,ENDING_INT);   //返回结果是300  
  33.    printf("%d /n",n);  
  34.   
  35.    n=SumAll(100,200);  //由于没有结束标识,返回结果不确定  
  36.    printf("%d /n",n);  
  37. }  

 

因为va_start, va_arg, va_end等定义成宏,所以可变参数的类型和个数需要由程序代码控制。
一般来说,设一个结束标识,这里是 ENDING_INT。用它来识别不同参数的个数。

  SumAll(100,200,ENDING_INT);   //调用方式正确,返回结果是300

  SumAll(100,200);  //调用方式不正确,由于没有结束标识,返回结果不确定。

三、理解va_start、va_arg和va_end

看一下<stdarg.h>中宏的定义:

定义:typedef char * va_list;
理解:va_list 就是一个指针,指向参数列表。

定义:#define _INTSIZEOF(n)   ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) – 1) )
定义:#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
理解:va_start 宏, 就是把ap赋值为参数v起始的参数列表的下一个参数

定义:#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) – _INTSIZEOF(t)) )
理解:va_arg宏,就是把ap赋值为下一个参数

定义:#define va_end(ap) ( ap = (va_list)0 )
理解:va_end宏,就是把ap赋值为空(0)

从va_*的实现可以看出,充分运用指针,把C语言的灵活特性表现得淋漓尽致。当然,用不好也容易出错。
va_*中,为了得到所有传递给函数的参数,需要用va_arg依次遍历。但是有两个要求:

(1)要确定参数的类型
    一般来说,各个参数的类型是一样的。

(2)要有结束标志。如果没有结束标志,va将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。
     所以上述的调用 SumAll(100,200);  返回的结果是不确定的。

四、再写一个参数类型是 char * 的 可变参数函数.

C++代码
  1. #include <stdio.h>  
  2. #include <string.h>  
  3. #include <alloc.h>  
  4. #include <stdarg.h>  
  5.   
  6. #define ENDING_STRING NULL  
  7.   
  8. //把多个字符串连接起来  
  9. char *StrCat(char *src,…)  
  10. {  
  11.     va_list     va;  
  12.     const char  *src_pointer;  
  13.     char        *dest;         /*  结果字符串 */  
  14.     size_t       dest_size;     /*  结果字符串的大小*/  
  15.   
  16.     /* 计算字符串的大小 */  
  17.     va_start (va, src);             /*  开始变长参数处理 */  
  18.     src_pointer   = src;  
  19.     dest_size = 1;  
  20.     while (src_pointer!=ENDING_STRING)  /* ENDING_STRING == NULL */  
  21.       {  
  22.         dest_size += strlen (src_pointer);  
  23.         src_pointer = va_arg (va, char *);  /* 取下一个参数 */  
  24.       }  
  25.     va_end (va);                    /*  结束变长参数处理  */  
  26.   
  27.     /*  申请内存 */  
  28.     dest = malloc( dest_size );  
  29.     if (dest == NULL) return (NULL);  
  30.   
  31.     /*  逐个复制字串到结果字符串  */  
  32.     va_start (va, src);       /*  开始变长参数处理 */  
  33.     src_pointer  = src;  
  34.     dest [0] = '/0';         /* 先设置为空串 */  
  35.     while (src_pointer!=ENDING_STRING)   /* ENDING_STRING == NULL */  
  36.       {  
  37.         strcat (dest, src_pointer);      /*  复制字串到结果字符串  */  
  38.         src_pointer = va_arg (va, char *);  
  39.       }  
  40.     va_end (va);                      /*  结束变长参数处理  */  
  41.     return (dest);  
  42. }   
  43.   
  44. int main(int argc, char* argv[])  
  45. {  
  46.    int n;  
  47.    char *s;  
  48.   
  49.    s=StrCat("hello"," ","world",NULL);  
  50.    printf("%s /n",s);   //返回结果是 hello world  
  51.   
  52. }  

嗯,还是比较好用的,千万不要忘记: 调用时要加上结束标识符哦。

  可能要问,为什么 printf() 函数调用时没有结束标识符呢?

  了解了一下 printf()的源码,我是这样理解的:
    printf(char *format,…) 中,在format参数中,就可以判断出后续参数的个数和类型,因此不需要结束标识符就可以知道参数的个数了。比如:
   printf( "%s %d", "hello", 1);
   "%s %d"表明后续参数个数为2个,第一个是 string类型,第二个是 int 类型。

 试一下,如果写成这样
  printf( "%s %d %d", "hello", 1);

 "%s %d %d"表明后续参数个数为3个,实际上是两个,由于缺了一个,返回结果是不可确定的

 

C/C++ 函数参数 省略号(变参技术,va_start,va_arg,va_end用法)

先看如下的一个函数:

C++代码
  1. #include <stdarg.h> // 必须包含的头文件  
  2.   
  3. int Add(int start,…) // …是作为占位符  
  4. {  
  5. va_list arg_ptr; // 定义变参起始指针  
  6. int sum=0; // 定义变参的和  
  7. int nArgValue =start; //  
  8.   
  9. va_start(arg_ptr,start); // arg_ptr指向第一个变参  
  10. do  
  11. {  
  12. sum+=nArgValue; // 求和  
  13. nArgValue = va_arg(arg_ptr,int); // arg_ptr指向下一个变参  
  14. }  
  15. while(nArgValue != 0); // 判断结束条件;结束条件是自定义为=0时结束  
  16.   
  17. va_end(arg_ptr); // 复位指针  
  18. return sum;  
  19. }  

函数的调用方法为Add(1,2,3,0);这样,必须以0结尾,因为变参函数结束的判断条件就是读到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 );

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)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。

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的起始地址
|————– -| ……………..
|————————– |
| |
|————— | -> 低内存地址处

(3) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) – _INTSIZEOF(t)) )
这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。

(4)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

点赞 (0)

发表评论

电子邮件地址不会被公开。 必填项已用*标注