va_list va_start va_end的使用

<pre name="code" class="cpp" style="color: rgb(51, 51, 51); white-space: pre-wrap; word-wrap: break-word;"><strong>一、 从printf()开始</strong>

从大家都很熟悉的格式化字符串函数开始介绍可变参数函数。
原型:int printf(const char * format, ...);
参数format表示如何来格式字符串的指令,
表示可选参数,调用时传递给"..."的参数可有可无,根据实际情况而定。
系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。
int vprintf / vscanf(const char * format, va_list ap); // 从标准输入/输出格式化字符串 
int vfprintf / vfsacanf(FILE * stream, const char * format, va_list ap); // 从文件流 
int vsprintf / vsscanf(char * s, const char * format, va_list ap); // 从字符串

  // 例1:格式化到一个文件流,可用于日志文件FILE *logfile;int <strong><span style="color:#ff0000;">WriteLog</span></strong>(<strong>const char * format, ...</strong>) //<strong>int i,...</strong>{va_list arg_ptr; //第一步:定义这个指向参数列表的变量va_start(arg_ptr, format);//第二步:把上面这个变量初始化,即让它指向参数列表int nWrittenBytes = vfprintf(logfile, format, arg_ptr);va_end(arg_ptr);//第四步:做一些清理工作return nWrittenBytes;}…// 调用时,与使用printf()没有区别。<strong><span style="color:#ff0000;">WriteLog</span></strong>("%04d-%02d-%02d %02d:%02d:%02d  %s/%04d logged out.",nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID);

同理,也可以从文件中执行格式化输入;或者对标准输入输出,字符串执行格式化。
在上面的例1中,WriteLog()函数可以接受参数个数可变的输入,本质上,它的实现需要vprintf()的支持。如何真正实现属于自己的可变参数函数,包括控制每一个传入的可选参数。

 

二、 va函数的定义和va宏

C语言支持va函数,作为C语言的扩展--C++同样支持va函数,但在C++中并不推荐使用,C++引入的多态性同样可以实现参数个数可变的函数。不 过,C++的重载功能毕竟只能是有限多个可以预见的参数个数。比较而言,C中的va函数则可以定义无穷多个相当于C++的重载函数,这方面C++是无能为 力的。va函数的优势表现在使用的方便性和易用性上,可以使代码更简洁。C编译器为了统一在不同的硬件架构、硬件平台上的实现,和增加代码的可移植性,提 供了一系列宏来屏蔽硬件环境不同带来的差异。
ANSI C标准下,va的宏定义在stdarg.h中,它们有:va_list,va_start(),va_arg(),va_end()。 

// 例2:求任意个自然数的平方和:
int SqSum(int n1, ...)
{
<strong>va_list </strong>arg_ptr;
int nSqSum = 0, n = n1;
va_start(arg_ptr, n1);
while (n > 0)
{nSqSum += (n * n);n = va_arg(arg_ptr, int);
}
va_end(arg_ptr);
return nSqSum;
}
// 调用时
int nSqSum = SqSum(7, 2, 7, 11, -1);

可变参数函数的原型声明格式为:
type VAFunction(type arg1, type arg2, … );
参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用"…"表示。固定参数和可选参数公同构成一个函数的参数列表。
借助上面这个简单的例2,来看看各个va_xxx的作用。 
va_list arg_ptr:定义一个指向个数可变的参数列表指针; 
va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,说明:argN是位于第一个可选参数之前的固定参数,(或者说,最后一个 固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一va函数的声明是void va_test(char a, char b, char c, …),则它的固定参数依次是a,b,c,最后一个固定参数argN为c,因此就是va_start(arg_ptr, c)。
va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。
va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。
va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。说明:指针arg_ptr被置无效后,可以通过调用 va_start()、va_copy()恢复arg_ptr。每次调用va_start() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。

 

 

三、 编译器如何实现va

例2中调用SqSum(7, 2, 7, 11, -1)来求7, 2, 7, 11的平方和,-1是结束标志。
简单地说,va函数的实现就是对参数指针的使用和控制。
typedef char *  va_list;  // x86平台下va_list的定义
函数的固定参数部分,可以直接从函数定义时的参数名获得;对于可选参数部分,先将指针指向第一个可选参数,然后依次后移指针,根据与结束标志的比较来判断是否已经获得全部参数。因此,va函数中结束标志必须事先约定好,否则,指针会指向无效的内存地址,导致出错。
这里,移动指针使其指向下一个参数,那么移动指针时的偏移量是多少呢,没有具体答案,因为这里涉及到内存对齐(alignment)问题,内存对齐跟具体 使用的硬件平台有密切关系,比如大家熟知的32位x86平台规定所有的变量地址必须是4的倍数(sizeof(int) = 4)。va机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va的可移植性无从谈起。
首先介绍宏_INTSIZEOF(n),它求出变量占用内存空间的大小,是va的实现的基础。
#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 )                           // 将指针置为无效
下表是针对函数int TestFunc(int n1, int n2, int n3, …) 参数传递时的内存堆栈情况。(C编译器默认的参数传递方式是__cdecl。)
对该函数的调用为int result = TestFunc(a, b, c, d. e); 其中e为结束标志。


从上图中可以很清楚地看出va_xxx宏如此编写的原因。
1. va_start。为了得到第一个可选参数的地址,我们有三种办法可以做到:
A) = &n3 + _INTSIZEOF(n3) 
// 最后一个固定参数的地址 + 该参数占用内存的大小 
B) = &n2 + _INTSIZEOF(n3) + _INTSIZEOF(n2) 
// 中间某个固定参数的地址 + 该参数之后所有固定参数占用的内存大小之和 
C) = &n1 + _INTSIZEOF(n3) + _INTSIZEOF(n2) + _INTSIZEOF(n1) 
// 第一个固定参数的地址 + 所有固定参数占用的内存大小之和 
从编译器实现角度来看,方法B),方法C)为了求出地址,编译器还需知道有多少个固定参数,以及它们的大小,没有把问题分解到最简单,所以不是很聪明的途 径,不予采纳;相对来说,方法A)中运算的两个值则完全可以确定。va_start()正是采用A)方法,接受最后一个固定参数。调用 va_start()的结果总是使指针指向下一个参数的地址,并把它作为第一个可选参数。在含多个固定参数的函数中,调用va_start()时,如果不 是用最后一个固定参数,对于编译器来说,可选参数的个数已经增加,将给程序带来一些意想不到的错误。(当然如果你认为自己对指针已经知根知底,游刃有余, 那么,怎么用就随你,你甚至可以用它完成一些很优秀(高效)的代码,但是,这样会大大降低代码的可读性。)
注意:宏va_start是对参数的地址进行操作的,要求参数地址必须是有效的。一些地址无效的类型不能当作固定参数类型。比如:寄存器类型,它的地址不是有效的内存地址值;数组和函数也不允许,他们的长度是个问题。因此,这些类型时不能作为va函数的参数的。
2. va_arg身兼二职:返回当前参数,并使参数指针指向下一个参数。
初看va_arg宏定义很别扭,如果把它拆成两个语句,可以很清楚地看出它完成的两个职责。
#define va_arg(ap,t)   ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址
// 将( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )拆成:
/* 指针ap指向下一个参数的地址 */
1.        ap += _INTSIZEOF(t);        // 当前,ap已经指向下一个参数了
/* ap减去当前参数的大小得到当前参数的地址,再强制类型转换后返回它的值 */
2.        return *(t *)( ap - _INTSIZEOF(t)) 
回想到printf/scanf系列函数的%d %s之类的格式化指令,我们不难理解这些它们的用途了- 明示参数强制转换的类型。
(注:printf/scanf没有使用va_xxx来实现,但原理是一致的。)
3.va_end很简单,仅仅是把指针作废而已。
#define va_end(ap) (ap = (va_list)0) // x86平台

四、 简洁、灵活,也有危险

从va的实现可以看出,指针的合理运用,把C语言简洁、灵活的特性表现得淋漓尽致,叫人不得不佩服C的强大和高效。不可否认的是,给编程人员太多自由空间必然使程序的安全性降低。va中,为了得到所有传递给函数的参数,需要用va_arg依次遍历。其中存在两个隐患:
1)如何确定参数的类型。 va_arg在类型检查方面与其说非常灵活,不如说是很不负责,因为是强制类型转换,va_arg都把当前指针所指向的内容强制转换到指定类型;
2)结束标志。如果没有结束标志的判断,va将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。例2中SqSum()求的是自然数的平方 和,所以我把负数和0作为它的结束标志。例如scanf把接收到的回车符作为结束标志,大家熟知的printf()对字符串的处理用'\0'作为结束标 志,无法想象C中的字符串如果没有'\0', 代码将会是怎样一番情景,估计那时最流行的可能是字符数组,或者是malloc/free。
允许对内存的随意访问,会留给不怀好意者留下攻击的可能。当处理cracker精心设计好的一串字符串后,程序将跳转到一些恶意代码区域执行,以使cracker达到其攻击目的。(常见的exploit攻击)所以,必需禁止对内存的随意访问和严格控制内存访问边界。

五、 Unix System V兼容方式的va声明

上面介绍可变参数函数的声明是采用ANSI标准的,Unix System V兼容方式的声明有一点点区别,它增加了两个宏:va_alist,va_dcl。而且它们不是定义在stdarg.h中,而是varargs.h中。 stdarg.h是ANSI标准的;varargs.h仅仅是为了能与以前的程序保持兼容而出现的,现在的编程中不推荐使用。
va_alist:函数声明/定义时出现在函数头,用以接受参数列表。
va_dcl:对va_alist的声明,其后无需跟分号";"
va_start的定义也不相同。因为System V可变参数函数声明不区分固定参数和可选参数,直接对参数列表操作。所以va_start()不是va_start(ap,v),而是简化为va_start(ap)。其中,ap是va_list型的参数指针。
Unix System V兼容方式下函数的声明形式:
type VAFunction(va_alist)
va_dcl  // 这里无需分号
{
    // 函数体内同ANSI标准
}

// 例3:猜测execl的实现(Unix System V兼容方式),摘自SUS V2
#include 
#define MAXARGS    100
/ * execl(file, arg1, arg2, ..., (char *)0); */
execl(va_alist)
va_dcl
{va_list ap;char *file;char *args[MAXARGS];int argno = 0;va_start(ap);file = va_arg(ap, char *);while ((args[argno++] = va_arg(ap, char *)) != (char *)0);va_end(ap);return execv(file, args);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/458706.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux学习之CentOS(三)----将Cent0S 7的网卡名称eno16777736改为eth0

【正文】 Linux系统版本&#xff1a;CentOS_7&#xff08;64位&#xff09; 一、前言&#xff1a; 今天又从Centos 6.5装回了Centos 7&#xff0c;毕竟还是要顺应潮流嘛。安装完成之后&#xff0c;发现发现CentOS 7默认的网卡名称是eno16777736&#xff0c;如图所示&#xff1a…

本地音频播放,使用AVFoundation.framework中的AVAudioPlayer来实现

本地音频播放,使用AVfoundation.framework中的AVAudioPlayer来实现 /*AVAudioPlayer的使用比较简单: 1、初始化AVAudioPlayer对象&#xff0c;此时通常指定本地文件路径 2、设置播放器属性&#xff0c;例如重复次数、音量大小等 3、调用play方法播放。 */

AngularJS操作DOM——angular.element

addClass()-为每个匹配的元素添加指定的样式类名after()-在匹配元素集合中的每个元素后面插入参数所指定的内容&#xff0c;作为其兄弟节点append()-在每个匹配元素里面的末尾处插入参数内容attr() - 获取匹配的元素集合中的第一个元素的属性的值bind() - 为一个元素绑定一个事…

C++中operator的主要用法

1&#xff0e; operator 用于类型转换函数&#xff1a; 类型转换函数的特征&#xff1a; 1&#xff09; 型转换函数定义在源类中&#xff1b; 2&#xff09; 须由 operator 修饰&#xff0c;函数名称是目标类型名或目标类名&#xff1b; 3&#xff09; 函数没有参数&#x…

声纹识别

一、 声纹识别是一项根据语音波形中反映说话人生理和行为特征的语音参数&#xff0c;自动识别说话人身份的技术。与语音识别不同的是&#xff0c;声纹识别利用的是语音信号中的说话人身份信息&#xff0c;而不考虑语音中的字词意思。由于每个人的生物特征具有与其他人不同的唯一…

Asp.net mvc 实时生成缩率图到硬盘

之前对于缩率图的处理是在图片上传到服务器之后&#xff0c;同步生成两张不同尺寸的缩率供前端调用&#xff0c;刚开始还能满足需求&#xff0c;慢慢的随着前端展示的多样化&#xff0c;缩率图已不能前端展示的需求&#xff0c;所以考虑做一个实时生成图片缩率图服务。 每次调用…

数据库事务的隔离机制

数据库事务(Database Transaction) &#xff0c;是指作为单个逻辑工作单元执行的一系列操作&#xff0c;要么完全地执行&#xff0c;要么完全地不执行。----百度百科就是说你定义一组数据库操作&#xff0c;然后告诉数据库说这些操作要么都成功&#xff0c;要么都不成功。类似于…

如何使用CppUnit进行单元测试

http://www.vckbase.com/document/viewdoc/?id1762 一、前言 测试驱动开发(TDD)是以测试作为开发过程的中心&#xff0c;它坚持&#xff0c;在编写实际代码之前&#xff0c;先写好基于产品代码的测试代码。开发过程的目标就是首先使测试能够通过&#xff0c;然后再优化设计结构…

录制wav格式的音频

项目中有面部认证、声纹认证&#xff0c;服务器端要求上传wav格式的音频&#xff0c;所以写了这样一个小demo。 刚刚开始写博客还不知道怎么上传代码&#xff0c;就复制了&#xff0c;嘻嘻 DotimeManage.h class DotimeManage; protocol DotimeManageDelegate <NSObject&g…

iOS开发网络篇—Reachability检测网络状态

前言&#xff1a;当应用程序需要访问网络的时候&#xff0c;它首先应该检查设备的网络状态&#xff0c;确认设备的网络环境及连接情况&#xff0c;并针对这些情况提醒用户做出相应的处理。最好能监听设备的网络状态的改变&#xff0c;当设备网络状态连接、断开时&#xff0c;程…

网络七层协议 五层模型 TCP连接 HTTP连接 socket套接字

socket&#xff08;套接字&#xff09;是通信的基石&#xff0c;是支持TCP/IP协议的网络通信的基本操作单元&#xff0c;包含进行网络通信必须的五种信息&#xff1a;连接使用的协议&#xff0c;本地主机的IP地址&#xff0c;本地进程的协议端口&#xff0c;远地主机的IP地址&a…

[vs2010 project] CppUnit快速入门

简介 测试是软件开发过程中极其重要的一环&#xff0c;详尽周密的测试能够减少软件BUG&#xff0c;提高软件品质。测试包括单元测试、系统测试等。其中单元测试是指针对软件功能单元所作的测试&#xff0c;这里的功能单元可以是一个类的属性或者方法&#xff0c;测试的目的是看…

[javascript|基本概念|Number]学习笔记

Number类型的值&#xff1a;整数/浮点数值 整数 十进制 e.g.: var intNum 50; 八进制 (严格模式下无效,解析错误)字面值首位必须是0,之后的数字序列为0&#xff5e;7 e.g.: var intNum 070; //解析为十进制56 (如果字面值数值超出了范围&#xff0c;前导0将被忽略&#xf…

[转]深入理解linux内核list_head

http://blog.chinaunix.net/uid-27122224-id-3277511.html 深入理解linux内核list_head的实现 2012-07-17 17:37:01 分类&#xff1a; LINUX 前言&#xff1a;在linux源代码中有个头文件为list.h。很多linux下的源代码都会使用这个头文件&#xff0c;它里面定义 了一个结构,以及…

xcode左侧不显示工程文件目录,提示NO Filter Results

解决办法&#xff1a; What solved was to go to Navigate > Reveal in Project Navigator . After this, the structure appeared again.

【VC++技术杂谈005】如何与程控仪器通过GPIB接口进行通信

在工控测试系统中&#xff0c;经常需要使用到各类程控仪器&#xff0c;这些程控仪器通常具有GPIB、LAN、USB等硬件接口&#xff0c;计算机通过这些接口能够与其通信&#xff0c;从而实现自动测量、数据采集、数据分析和数据处理等操作。本文主要介绍如何与程控仪器通过GPIB接口…

标题在上边框中的html(fieldset标签)

<fieldset> <legend>标题</legend> 内容 </fieldset> 转载于:https://www.cnblogs.com/lswbk/p/4952820.html

移除项目中的CocoaPods

在项目中移除CocoaPods cocoaPods虽然很方便&#xff0c;但是我是真心的不喜欢用它&#xff0c;总是出错如果你觉得CocoaPods让你的项目出现了问题&#xff0c;不好用甚至是恶心&#xff0c;想将其从项目中彻底移除&#xff0c;也有方法&#xff1a; 1.删除工程文件夹下的Podf…

ShellExecute使用详解

有三个API函数可以运行可执行文件WinExec、ShellExecute和CreateProcess。 1.CreateProcess因为使用复杂&#xff0c;比较少用。 2.WinExec主要运行EXE文件。如&#xff1a;WinExec(Notepad.exe Readme.txt, SW_SHOW); 3.ShellExecute不仅可以运行EXE文件&#xff0c;也可以运行…

javascript笔记整理(对象基础)

一、名词解释 1.基于对象&#xff08;一切皆对象&#xff0c;以对象的概念来编程&#xff09; 2.面向对象编程(Object Oriented Programming&#xff0c;OOP) A.对象(JavaScript 中的所有事物都是对象) B.对象的属性和行为 属性:用数据值来描述他的状态 行为:用来改变对象行为的…