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的定义就够了,也便于程序的移植.

(0)

相关推荐

  • 关于C/C++中可变参数的详细介绍(va_list,va_start,va_arg,va_end)

    由于在C语言中没有函数重载,解决不定数目函数参数问题变得比较麻烦,即使采用C++,如果参数个数不能确定,也很难采用函数重载.对这种情况,提出了指针参数来解决问题. 如printf()函数,其原型为:int   printf(   const   char*   format,   ...); 它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的,例如我们可以有以下不同的调用方法:   printf( "%d ",i);   printf( "%s "

  • 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中,所以用到可变参数的程序

  • C/C++中可变参数的用法详细解析

    可变参数即表示参数个数可以变化,可多可少,也表示参数的类型也可以变化,可以是int,double还可以是char*,类,结构体等等.可变参数是实现printf(),sprintf()等函数的关键之处,也可以用可变参数来对任意数量的数据进行求和,求平均值带来方便(不然就用数组或每种写个重载).在C#中有专门的关键字parame,但在C,C++并没有类似的语法,不过幸好提供这方面的处理函数,本文将重点介绍如何使用这些函数. 第一步 可变参数表示用三个点-来表示,查看printf()函数和scanf(

  • Mysql中复制详细解析

    1.mysql复制概念 指将主数据库的DDL和DML操作通过二进制日志传到复制服务器上,然后在复制服务器上将这些日志文件重新执行,从而使复制服务器和主服务器的数据保持同步.复制过程中一个服务器充当主服务器(master),而一个或多个其它服务器充当从服务器(slaves).主服务器将更新重新写入二进制日志文件,并维护文件的一个索引以跟踪日志循环.这些日志可以记录发送到从服务器的更新.当一个从服务器连接主服务器时,它通知主服务器.从服务器在日志中读取的最后一次成功更新的位置.从服务器接受从那时起发

  • 远程数据库的表超过20个索引的影响详细解析

    昨天同事参加了一个研讨会,有提到一个案例.一个通过dblink查询远端数据库,原来查询很快,但是远端数据库增加了一个索引之后,查询一下子变慢了. 经过分析,发现那个通过dblink的查询语句,查询远端数据库的时候,是走索引的,但是远端数据库添加索引之后,如果索引的个数超过20个,就会忽略第一个建立的索引,如果查询语句恰好用到了第一个建立的索引,被忽略之后,只能走Full Table Scan了. 听了这个案例,我查了一下,在oracle官方文档中,关于Managing a Distributed

  • MySQL DELETE语法使用详细解析

    以下的文章主要描述的是MySQL DELETE语法的详细解析,首先我们是从单表语法与多表语法的示例开始的,假如你对MySQL DELETE语法的相关内容十分感兴趣的话,你就可以浏览以下的文章对其有个更好的了解. 单表语法: 复制代码 代码如下: DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM tbl_name   [WHERE where_definition]   [ORDER BY ...]   [LIMIT row_count] 多表语法: 复制代码

  • Tomcat中的catalina.bat原理详细解析

    前言 本文主要给大家详细解析了关于Tomcat中catalina.bat原理的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. tomcat 的真正启动是在 catalina.bat 设置并启动的.startup.bat 只是找到catalina.bat 然后执行catalina.bat 来启动tomat的.下面我们来分析下catalina.bat 验证CATALINA_HOME 环境变量 验证CATALINA_HOME 设置是否正确,如果不正确,重新设置CATALIN

  • 对MySQL配置参数 my.ini/my.cnf的详细解析

    以下的文章主要描述的是对MySQL配置参数 my.ini/my.cnf的详细解析,我们主要是以实例演示的方式来对MySQL配置参数 my.ini/my.cnf的实际操作步骤进行说明,以下就是相关内容的具体描述. 1.获取当前配置参数 要优化MySQL配置参数,首先要了解当前的配置参数以及运行情况.使用下列命令可以获得目前服务器使用的配置参数: 复制代码 代码如下: mysqld –verbose –help mysqladmin variables extended-status –u root

  • log4j使用详细解析

    简单的说log4j就是帮助开发人员进行日志输出管理的API类库.它最重要的特点就可以配置文件灵活的设置日志信息的优先级.日志信息的输出目的地.日志信息的输出格式 Log4j 除了可以记录程序运行日志信息外还有一重要的功能就是用来显示调试信息. 程序员经常会遇到脱离Java ide环境调试程序的情况,这时大多数人会选择使用System.out.println语句输出某个变量值的方法进行调试.这样会带来一个非常麻烦的问题:一旦哪天程序员决定不要显示这些System.out.println的东西了就只

  • Mybatis中的config.xml配置文件详细解析

    经过前面的文章,我觉得对Mybatis的正题理解已经足够了,但是对Mybatis的使用,我觉得还是会有一点的模糊,就我个人而言,我觉得掌握好Mybatis框架,主要要明白三个文件,第一个就是等下要谈论的Mybatis-comfig.xml文件,还有就是**Mapper.xml,以及我们所定义的Mapper类,理解了这三个东西,然后有sql的基础,还有java的基础的话,后面不论是使用基于xml的方法,还是基于java-based Configuration的方法,都会简单的多. 废话不多说,现在

随机推荐