va_start(),va_end()函数应用

本文详细解析了C语言中处理变参问题的VA_LIST宏的应用,包括其工作原理、编译器处理过程以及使用注意事项。通过实例展示了如何使用VA_LIST宏处理可变参数函数,包括声明、初始化、获取参数和结束参数列表的过程。同时讨论了可变参数函数的优缺点,建议在必要场合使用,并对比了C++多态性在实现可变参数功能的优势。
va_start(),va_end()函数应用

原理解释:

VA_LIST 是在C语言中解决变参问题的一组宏,在<stdarg.h>头文件下。

VA_LIST的用法:      
      (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针
      (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。
      (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。
      (4)最后用VA_END宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用VA_ARG获取各个参数。

VA_LIST在编译器中的处理:

 (1)在运行VA_START(ap,v)以后,ap指向第一个可变参数在堆栈的地址。
(2)VA_ARG()取得类型t的可变参数值,在这步操作中首先apt = sizeof(t类型),让ap指向下一个参数的地址。然后返回ap-sizeof(t类型)的t类型*指针,这正是  第一个可变参数在堆栈里的地址。然后用*取得这个地址的内容。
(3)VA_END(),X86平台定义为ap = ((char*)0),使ap不再指向堆栈,而是跟NULL一样,有些直接定义为((void*)0),这样编译器不会为VA_END产生代码,例如gcc在Linux的X86平台就是这样定义的。

要注意的是:由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。

使用VA_LIST应该注意的问题:
   (1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
   (2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
 

小结:可变参数的函数原理其实很简单,而VA系列是以宏定义来定义的,实现跟堆栈相关。我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数,如果在C++里,我们应该利用C++多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。

va_list ap; //声明一个变量来转换参数列表   
va_start(ap,fmt);          //初始化变量   
va_end(ap);     //结束变量列表,和va_start成对使用   
可以根据va_arg(ap,type)取出参数  

已经经过调试成功的输出程序

#include<stdio.h>
#include <stdarg.h>

#define bufsize 80
char buffer[bufsize];

int vspf(char *fmt, ...)
{
     va_list argptr;
     int cnt;
    va_start(argptr, fmt);

     cnt = vsnprintf(buffer,bufsize ,fmt, argptr);

     va_end(argptr);

    return(cnt);
}

int main(void)
{
     int inumber = 30;

     float fnumber = 90.0;

     char string[4] = "abc";

    vspf("%d %f %s", inumber, fnumber, string);

    printf("%s\n", buffer);

    return 0;
}

运行结果为:

30 90.000000 abc

 

vsnprintf:int vsnprintf(char *str, size_t size, const char *format, va_list ap);
      write output to character sting str

      return value:the number of characters
       printed (not including the trailing '\0' used to end output to strings). The functions snprintf() and vsnprintf() do not write more than size bytes (including the trailing '\0'). If the output was truncated due to this limit then the return value is the number of characters (not including the trailing '\0') which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated. If an output error is encountered, a negative
value is returned.
                   if (return_value > -1)   
                       size = n+1;
                    else          
                       size *= 2;
The glibc implementation of the functions snprintf() and vsnprintf() conforms to the C99 standard, i.e., behaves as described above, since   glibc version 2.1. Until glibc 2.0.6 they would return -1 when the out put was truncated.
C语言用va_start等宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参

数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。 在stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:

       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 )

  _INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。

  为了能从固定参数依次得到每个可变参数,va_start,va_arg充分利用下面两点:

  1. C语言在函数调用时,先将最后一个参数压入栈

  2. X86平台下的内存分配顺序是从高地址内存到低地址内存

  高位地址

  第N个可变参数

  。。。

  第二个可变参数

  第一个可变参数      ? ap

  固定参数           ? v

  低位地址

  由上图可见,v是固定参数在内存中的地址,在调用va_start后,ap指向第一个可变参数。这个宏的作用就是在v的内存地址上增加v所占的内存大小,这样就得到了第一个可变参数的地址。

  接下来,可以这样设想,如果我能确定这个可变参数的类型,那么我就知道了它占用了多少内存,依葫芦画瓢,我就能得到下一个可变参数的地址。

  让我再来看看va_arg,它先ap指向下一个可变参数,然后减去当前可变参数的大小即得到当前可变参数的内存地址,再做个类型转换,返回它的值。

  要确定每个可变参数的类型,有两种做法,要么都是默认的类型,要么就在固定参数中包含足够的信息让程序可以确定每个可变参数的类型。比如,printf,程序通过分析format字符串就可以确定每个可变参数大类型。

  最后一个宏就简单了,va_end使得ap不再指向有效的内存地址。

  其实在varargs.h头文件中定义了UNIX System V实行的va系列宏,而上面在stdarg.h头文件中定义的是ANSI C形式的宏,这两种宏是不兼容的,一般说来,我们应该使用ANSI C形式的va宏。

 

定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函数是从右向左压入堆栈的,函数的参数在堆栈中的分布位置.我
  们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的
  地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址:
  高地址|-----------------------------|
  |函数返回地址 |
  |-----------------------------|
  |....... |
  |-----------------------------|
  |第n个参数(第一个可变参数) |
  |-----------------------------|<--va_start后ap指向
  |第n-1个参数(最后一个固定参数)|
  低地址|-----------------------------|<-- &v
  然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:
  j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) ); 首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回
  ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址
  然后用*取得这个地址的内容(参数值)赋给j.
  高地址|-----------------------------|
  |函数返回地址 |
  |-----------------------------|
  |....... |
  |-----------------------------|<--va_arg后ap指向
  |第n个参数(第一个可变参数) |
  |-----------------------------|<--va_start后ap指向
  |第n-1个参数(最后一个固定参数)|
  低地址|-----------------------------|<-- &v
  最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不
  会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.关于va_start, va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
System V Unix把va_start定义为只有一个参数的宏:
  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++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现


va_start()va_end()函数应用:
man:
       #include <stdarg.h>

       void va_start(va_list ap, last);
       type va_arg(va_list ap, type);
       void va_end(va_list ap);
       void va_copy(va_list dest, va_list src);
1:当无法列出传递函数的所有实参的类型和数目时,可用省略号指定参数表
void foo(...);
void foo(parm_list,...);

2:函数参数的传递原理
函数参数是以数据结构:栈的形式存取,从右至左入栈.
eg:
#include   
void fun(int a, ...)
{
int *temp = &a;
temp++;
for (int i = 0; i < a; ++i)
{
cout << *temp << endl;
temp++;
}
}
int main()
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
fun(4, a, b, c, d);
system("pause");
return 0;
}
Output::
1
2
3
4

3:获取省略号指定的参数
在函数体中声明一个va_list,然后用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束。

4.va_start使argp指向第一个可选参数。va_arg返回参数列表中的当前参数并使argp指向参数列表中的下一个参数。va_end把argp指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。

实例:
编写vstart.c,如下:

//vstart.c
#include <stdio.h>
#include <strings.h>
#include <stdarg.h>
int demo(char *fmt, ...);
int main() 
{ 
   demo("DEMO", "This", "is", "a", "demo!", ""); 
   return 0;
} 
int demo( char *fmt, ... ) 
{ 
   va_list argp; 
   int argno = 0;  
   char *para; 
   va_start(argp, fmt); 
   while (1) 
   { 
      para = va_arg(argp, char *); 
      if (strcmp( para, "") == 0) 
      break; 
      printf("Parameter #%d is: %s/n", argno, para); 
      argno++; 
   } 
   va_end( argp ); 
   return 0; 
} 

  运行结果如下:

Parameter #0 is: This
Parameter #1 is: is
Parameter #2 is: a
Parameter #3 is: demo!

<think>在之前的对话中,用户没有提出过关于可变参数的问题,这是首次询问。因此,我将根据提供的引用内容,系统地解释va_startva_arg、va_end的使用方法、原理及示例。 首先,这些宏定义在<stdarg.h>头文件中,用于处理可变参数列表。以下是核心组件的说明: 1. **va_list**:这是一个类型,用于声明一个指针变量,该变量将指向可变参数列表。 2. **va_start**:初始化va_list指针,使其指向可变参数列表的第一个参数。 3. **va_arg**:获取当前参数的值,并将指针移动到下一个参数。 4. **va_end**:清理工作,结束可变参数列表的访问。 ### 使用步骤: 1. 在函数中声明一个va_list类型的变量。 2. 使用va_start初始化该变量,需要传入最后一个固定参数。 3. 使用va_arg逐个读取参数,需指定参数类型。 4. 使用va_end结束访问。 ### 原理: 可变参数的实现依赖于函数调用时参数在栈上的布局。固定参数之后的参数在内存中连续存放。va_start通过最后一个固定参数的地址计算出第一个可变参数的地址。va_arg根据类型大小移动到下一个参数。va_end用于清理状态,防止野指针。 ### 示例: 参考引用[2]和[4],以下是一个简单示例: ```c #include <stdio.h> #include <stdarg.h> void print_ints(int num, ...) { va_list args; va_start(args, num); // 初始化,num是最后一个固定参数 for (int i = 0; i < num; i++) { int value = va_arg(args, int); // 按int类型获取参数 printf("%d ", value); } va_end(args); // 清理 } int main() { print_ints(3, 10, 20, 30); // 输出:10 20 30 return 0; } ``` ### 注意事项: - 必须提供至少一个固定参数,用于va_start定位。 - 读取参数的类型必须与实际传入类型匹配,否则行为未定义。 - 读取的参数数量不能超过实际传递的数量。 - 结束时应调用va_end。 ### 其他应用: 如引用[3]所示,这些宏可用于实现格式化输出函数(如printf的变体): ```c void my_printf(const char *fmt, ...) { char buf[BUF_SIZE]; va_list ap; va_start(ap, fmt); vsnprintf(buf, BUF_SIZE, fmt, ap); // 使用可变参数格式化字符串 va_end(ap); // 处理buf... } ``` ### 引用标记: - va_list、va_start等宏用于处理可变参数列表[^1]。 - 使用va_arg遍历参数时需指定类型[^2]。 - 实现原理涉及栈内存布局和指针运算[^4]。 §§相关问题§§ 1. 可变参数函数的类型安全如何保证? 2. 如何在可变参数函数中处理不同类型的参数? 3. va_list能否在多个函数间传递? 4. 为什么需要调用va_end? </think> ### va_startva_arg、va_end 详解 **定义**:`va_list`、`va_start`、`va_arg`、`va_end` 是 C 语言中处理**可变参数列表**的宏(定义在 `<stdarg.h>` 头文件中),用于实现函数参数数量可变的功能(如 `printf`)[^1]。 --- ### **核心原理** 1. **参数存储方式** 函数调用时,参数按从右到左顺序压入栈中。固定参数之后的可变参数在栈中连续存储。 - `va_list`:声明指针变量,指向可变参数列表。 - `va_start`:初始化指针,指向第一个可变参数地址。 - `va_arg`:获取当前参数值,并将指针后移到下一个参数。 - `va_end`:释放指针,防止野指针。 2. **内存对齐** 宏 `_INTSIZEOF(n)` 确保指针按类型大小对齐。例如,`int` 类型在 32 位系统中对齐到 4 字节。 --- ### **使用方法** #### 1. 基本步骤 ```c #include <stdarg.h> void func(int fixed_arg, ...) { va_list args; // 声明参数列表指针 va_start(args, fixed_arg); // 初始化指针(fixed_arg 是最后一个固定参数) // 用 va_arg 逐个读取参数(需指定类型) int val1 = va_arg(args, int); // 读取 int 类型参数 char* val2 = va_arg(args, char*); // 读取 char* 类型参数 va_end(args); // 结束使用,释放指针 } ``` #### 2. 示例代码 **示例1:读取多个整数**(引用自 ) ```c #include <stdio.h> #include <stdarg.h> void print_ints(int num_args, ...) { va_list args; va_start(args, num_args); for (int i = 0; i < num_args; i++) { printf("%d ", va_arg(args, int)); // 按 int 类型读取 } va_end(args); } int main() { print_ints(3, 1, 2, 3); // 输出:1 2 3 return 0; } ``` **示例2:模拟格式化输出**(引用自 [^3]) ```c #include <stdarg.h> #define BUF_SIZE 16 void my_printf(const char *fmt, ...) { char buf[BUF_SIZE]; va_list ap; va_start(ap, fmt); vsnprintf(buf, BUF_SIZE, fmt, ap); // 格式化字符串到 buf char *p = va_arg(ap, char*); // 读取额外参数 va_end(ap); } ``` --- ### **注意事项** 1. **参数类型匹配** `va_arg` 需明确指定参数类型(如 `int`、`char*`),若类型错误会导致未定义行为。 2. **参数数量控制** 需通过固定参数指定可变参数数量(如 `num_args`)或设置结束标记(如 `NULL`)。 3. **平台依赖性** 实现依赖栈内存布局,不同编译器/架构可能有差异。 4. **必须调用 `va_end`** 防止指针泄露,确保资源清理。 --- ### **典型应用场景** 1. 实现可变参数函数(如 `printf`、`scanf`)。 2. 封装日志输出、调试工具等通用模块。 3. 需要动态处理不同类型参数的场景(如序列化库)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值