va宏的用法 可变参数

2024-03-23 04:38
文章标签 参数 用法 可变 va

本文主要是介绍va宏的用法 可变参数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原理解释:

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在编译器中的处理:
 
va_list和vsnprintf

(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;
}

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 tostrings). The functions snprintf() and vsnprintf() do not writemore than size bytes (including the trailing '\0'). If the outputwas truncated due to this limit then the return value is the numberof characters (not including the trailing '\0') which would havebeen written to the final string if enough space had beenavailable. Thus, a return value of size or more means that theoutput was truncated. If an output error is encountered, anegative
value is returned.
                  if (return_value >-1)   
                      size = n+1;
                   else          
                      size *= 2;
The glibc implementation of the functions snprintf() andvsnprintf() conforms to the C99 standard, i.e., behaves asdescribed above, since   glibcversion 2.1. Until glibc 2.0.6 they would return -1 when the output 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) )

  #defineva_arg(ap,t)   ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

  #defineva_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 SystemV实行的va系列宏,而上面在stdarg.h头文件中定义的是ANSI C形式的宏,这两种宏是不兼容的,一般说来,我们应该使用ANSIC形式的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宏的用法 可变参数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/837160

相关文章

mapstruct中的@Mapper注解的基本用法

《mapstruct中的@Mapper注解的基本用法》在MapStruct中,@Mapper注解是核心注解之一,用于标记一个接口或抽象类为MapStruct的映射器(Mapper),本文给大家介绍ma... 目录1. 基本用法2. 常用属性3. 高级用法4. 注意事项5. 总结6. 编译异常处理在MapSt

java中long的一些常见用法

《java中long的一些常见用法》在Java中,long是一种基本数据类型,用于表示长整型数值,接下来通过本文给大家介绍java中long的一些常见用法,感兴趣的朋友一起看看吧... 在Java中,long是一种基本数据类型,用于表示长整型数值。它的取值范围比int更大,从-922337203685477

MyBatis ResultMap 的基本用法示例详解

《MyBatisResultMap的基本用法示例详解》在MyBatis中,resultMap用于定义数据库查询结果到Java对象属性的映射关系,本文给大家介绍MyBatisResultMap的基本... 目录MyBATis 中的 resultMap1. resultMap 的基本语法2. 简单的 resul

Python主动抛出异常的各种用法和场景分析

《Python主动抛出异常的各种用法和场景分析》在Python中,我们不仅可以捕获和处理异常,还可以主动抛出异常,也就是以类的方式自定义错误的类型和提示信息,这在编程中非常有用,下面我将详细解释主动抛... 目录一、为什么要主动抛出异常?二、基本语法:raise关键字基本示例三、raise的多种用法1. 抛

java中Optional的核心用法和最佳实践

《java中Optional的核心用法和最佳实践》Java8中Optional用于处理可能为null的值,减少空指针异常,:本文主要介绍java中Optional核心用法和最佳实践的相关资料,文中... 目录前言1. 创建 Optional 对象1.1 常规创建方式2. 访问 Optional 中的值2.1

git stash命令基本用法详解

《gitstash命令基本用法详解》gitstash是Git中一个非常有用的命令,它可以临时保存当前工作区的修改,让你可以切换到其他分支或者处理其他任务,而不需要提交这些还未完成的修改,这篇文章主要... 目录一、基本用法1. 保存当前修改(包括暂存区和工作区的内容)2. 查看保存了哪些 stash3. 恢

Python struct.unpack() 用法及常见错误详解

《Pythonstruct.unpack()用法及常见错误详解》struct.unpack()是Python中用于将二进制数据(字节序列)解析为Python数据类型的函数,通常与struct.pa... 目录一、函数语法二、格式字符串详解三、使用示例示例 1:解析整数和浮点数示例 2:解析字符串示例 3:解

C++/类与对象/默认成员函数@构造函数的用法

《C++/类与对象/默认成员函数@构造函数的用法》:本文主要介绍C++/类与对象/默认成员函数@构造函数的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录名词概念默认成员函数构造函数概念函数特征显示构造函数隐式构造函数总结名词概念默认构造函数:不用传参就可以

javascript fetch 用法讲解

《javascriptfetch用法讲解》fetch是一个现代化的JavaScriptAPI,用于发送网络请求并获取资源,它是浏览器提供的全局方法,可以替代传统的XMLHttpRequest,这篇... 目录1. 基本语法1.1 语法1.2 示例:简单 GET 请求2. Response 对象3. 配置请求

Go 语言中的 Struct Tag 的用法详解

《Go语言中的StructTag的用法详解》在Go语言中,结构体字段标签(StructTag)是一种用于给字段添加元信息(metadata)的机制,常用于序列化(如JSON、XML)、ORM映... 目录一、结构体标签的基本语法二、json:"token"的具体含义三、常见的标签格式变体四、使用示例五、使用