我的编程空间,编程开发者的网络收藏夹
学习永远不晚

va_list(),va_start(),va_arg(),va_end() 详细解析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

va_list(),va_start(),va_arg(),va_end() 详细解析

(一)写一个简单的可变参数的C函数

下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的C函数要在程序中用到以下这些宏:

复制代码 代码如下:

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中,所以用到可变参数的程序应该包含这个头文件.下面我们写一个简单的可变参数的函数,该函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
复制代码 代码如下:

void simple_va_fun(int i, ...)
{
    va_listarg_ptr;
    intj=0;

   va_start(arg_ptr, i);
   j=va_arg(arg_ptr, int);
   va_end(arg_ptr);
    printf("%d%d\n", i, j);
   return;
}


我们可以在我们的头文件中这样声明我们的函数:
extern void simple_va_fun(int i, ...);
我们在程序中可以这样调用:
simple_va_fun(100);
simple_va_fun(100,200);

从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.

2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.

3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.

4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.

如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:
1)simple_va_fun(100);
结果是:100 -123456789(会变的值)
2)simple_va_fun(100,200);
结果是:100 200
3)simple_va_fun(100,200,300);
结果是:100 200
我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果的原因和可变参数在编译器中是如何处理的.

(二)可变参数在编译器中的处理

我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于
1)硬件平台的不同
2)编译器的不同
所以定义的宏也有所不同,下面以VC++中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)主要是为了某些需要内存的对齐的系统.C语言的函数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再看va_start的定义,定义为
&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以我们运行va_start(ap,v)以后,ap指向第一个可变参数在堆
栈的地址,如图:

高地址
       |-----------------------------|
      |函数返回地址                 |
      |-----------------------------|
       |.......                     |
       |-----------------------------|
       |第n个参数(第一个可变参数)     |
       |-----------------------------|<--va_start后ap指向
      |第n-1个参数(最后一个固定参数)  |
      |-----------------------------|<--&v
低地址
              图( 1 )

然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址(图2).然后用*取得这个地址的内容(参数值)赋给j.

高地址
       |-----------------------------|
       |函数返回地址                  |
       |-----------------------------|
       |.......                      |
       |-----------------------------|<--va_arg后ap指向
       |第n个参数(第一个可变参数)     |
       |-----------------------------|<--va_start后ap指向
       |第n-1个参数(最后一个固定参数) |
       |-----------------------------|<--&v
低地址
       图( 2 )

最后要说的是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的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.

另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.如果simple_va_fun()改为:

复制代码 代码如下:

void simple_va_fun(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 %s\n", i, s);
return;
}


可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现core dump(Unix)或者页面非法的错误(window平台).但也有可能不出错,但错误却是难以发现,不利于我们写出高质量的程序.

以下提一下va系列宏的兼容性.
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的宏跟systemV的宏是不兼容的,我们一般都用ANSI C,所以用ANSI C的定义就够了,也便于程序的移植.

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

va_list(),va_start(),va_arg(),va_end() 详细解析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

va_list(),va_start(),va_arg(),va_end() 详细解析

这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.下面我们写一个简单的可变参数的函数,该函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值
2022-11-15

window.history详细解析

window.history是JavaScript中的一个对象,表示浏览器的历史记录。它提供了一些方法和属性,允许开发人员在浏览器历史记录中进行导航和操作。window.history对象的属性包括:- length:返回浏览器历史记录中的
2023-09-13

SQL Optimizer 详细解析

目录一、 大数据体系和SQL1、SQL的处理流程1.1 Parser1.2 Analyzer和Logical Plan1.3 Physical Plan 和 Executor1.4 小结二、 常见的查询优化器1、查询优化器分类2、RBO(R
2022-07-26

log4j使用详细解析

简单的说log4j就是帮助开发人员进行日志输出管理的API类库。它最重要的特点就可以配置文件灵活的设置日志信息的优先级、日志信息的输出目的地、日志信息的输出格式Log4j 除了可以记录程序运行日志信息外还有一重要的功能就是用来显示调试信息。
2023-05-31

treeshaking功能详细解析

这篇文章主要为大家介绍了treeshaking功能详细解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-18

C/C++ 宏详细解析

关于宏的一些语法问题,可以在google上找到。相信我,你对于宏的了解绝对没你想象的那么多。如果你还不知道#和##,也不知道prescan,那么你肯定对宏的了解不够
2022-11-15

pcre函数详细解析

PCRE提供了19个接口函数,为了简单介绍,使用PCRE内带的测试程序(pcretest.c)示例用法
2022-11-15

c_str()的用法详细解析

c_str()就是把string类对象转换成和c兼容的char *类型。这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式
2022-11-15

CStdioFile的用法详细解析

CStdioFile 不支持Duplicate,LockRange,和UnlockRange 这几个CFile 函数。如果在CStdioFile 中调用了这几个函数,将会出现CNoSupported 异常
2022-11-15

React props全面详细解析

props 是 React 组件通信最重要的手段,它在 React 的世界中充当的角色是十分重要的。学好 props 可以使组件间通信更加灵活,同时文中会介绍一些 props 的操作技巧,和学会如何编写嵌套组件
2022-11-13

dword ptr指令详细解析

8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作
2022-11-15

Java Stream.reduce()用法详细解析

StreamAPI提供了丰富的中间函数,归并函数和终端函数,这些函数还支持并行化执行,下面这篇文章主要给大家介绍了关于Java Stream.reduce()用法的相关资料,需要的朋友可以参考下
2022-12-14

详细解析IPv6的名称

  IPv6是IETF(也就是互联网工程任务组)设计的用于替代现行版本IP协议(IPv4)的下一代IP协议,号称可以为全世界的每一粒沙子编上一个网址。今天小编要给大家分享一篇教程,那就是:Cisco网络协议之详细解析IPv6的名称。  一、IPv6地址别名  相信大家都应该知道IPv6被设计为与源和目标主机的128位I
详细解析IPv6的名称
2024-04-18

C语言printf详细解析

C中格式字符串printf()的一般形式为: %[标志][输出最小宽度][.精度][长度]类型, 其中方括号[]中的项为可选项。各项的意义介绍如下
2022-11-15

Java@GlobalLock注解详细分析讲解

这篇文章主要介绍了Java@GlobalLock注解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2022-11-21

产品数据分析的详细解析

  下面就是小编为大家准备的关于数据分析这一块,下面你就可以阅读文章了!随着业务的发展,以数据报表的形式来提供数据服务逐渐不能满足需求了。一方面,高层期望每天一早便能看到清晰的数据,搞清楚最近的运营效果和趋势;另一方面,虽然数据报表提供了详细的数据,但是还是需要手动去过滤、统计一下才有结果,所有想看数据的人都需要做一遍
产品数据分析的详细解析
2024-04-18

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录