关于:va_list,va_start,va_arg的3篇文章(ZZ) _char data[1024]; va_list argp; va_start(argp, fmt)-程序员宅基地

技术标签: 编译器  null  平台  gcc  VC++程序设计  list  fun  

 

 关于:va_list,va_start,va_arg的3篇文章(ZZ) 

文章1:C语言中变长参数(va_list,va_start,va_arg)沉思录
 
 
一.引言:
 
C语言中关于变长参数的使用很简单,无非是如下的框架。是否可以不用宏而编写处理变长参数的函数呢?答案是肯定的,本文作了一些处浅探讨,不足之处望各位批评指正。
 
使用宏的程序框架:
#include <stdio.h>
#include <stdart.h>  /* 或者#include <vararg.h> */
 
int print (char * fmt, ...)
{
    va_list args;
    ...
 
    va_start (args, fmt);
  
    /* do something here */
 
    va_end (args);
 
    /* do something here too */
}
 
我看了一下有关va_list, va_start, va_end宏的定义,各编译器不大一样,我着重研究了一下gcc的以上三个宏,并在不用三个宏的情况下编写了测试程序。测试过程和大家分享。
 
gcc中va_list的定义
#define char*  va_list   /* gcc中va_list等同char* */
 
gcc中三个宏的定义如下(经过我加工整理后):
#define va_start(AP, LASTARG) ( /
            AP = ((char *)& (LASTARG) + /
            __va_rounded_size(LASTARG)))
 
#define va_arg(AP, TYPE) ( /
            AP += __va_rounded_size(TYPE), /
            *((TYPE *)(AP - __va_rounded_size(TYPE))))
 
#define va_end(AP)              /* 没有定义,没有操作 */
有的编译器这样定义:
#define va_end(AP) ((void *)0)  /* 有定义,是空操作 */
 
二.不用宏的处理变长参数实践:
 
本人在分析了上面的宏后,在Linux、 i386、 gcc、 gas平台的Think Pad R50e上不用上面的三个宏成功写了个测试C代码,并详细分析了该代码的汇编代码,先给出代码如下:
 
/*****************************************************
 * File Name : mytest.c          
 * Copyright by : Superware   
 * Version : 0.01             
 *****************************************************/
 
#include <stdio.h>            /* 我没包含 stdarg.h 或 vararg.h */
 
void print (char * fmt, ...) {     char * arg;               /* 变长参数的指针
                                 相当于 va_list arg */     int i;                    /* 接受int参数 */     double d;                 /* 接受double参数 */     char c;                   /* 接受char 参数*/     char *s;                  /* 接受字符串 */
 
    printf ("%s", fmt);       /* 打印第一个参数 fmt串 */
 
    arg = (char *)&fmt + 4;   /* 相当于 va_start(arg, fmt)
                                 这里的 +4 实际上是
                                 sizeof(char *) 因为在IA32
                                 中,所以我写了4 没有考虑移植,
                                 为什么? 在下面解释,
                                 注意这里加 4表示arg已经指向
                                 第二个参数 */     /* 打印第二个参数 */     i = *(int *)arg;          /* 接受第二个参数,
                                 为了直接了当,我硬性规定 
                                 print()函数的第二个
                                 参数是整数,请看 */
                                 main()函数中的print()
                                 函数调用 */     printf ("%d", i);         /* 打印地二个参数,是整数,
                                 所以用格式"%d" */     arg += sizeof(int);       /* 指向下一个参数(第三个参数),
                                 为什么是加sizeof(int),
                                 分析汇编码你就明白了 */
    /* 打印第三个参数 */
    d = *(double *)arg;       /* 以下的解释同地二个参数类似,
                                 就不详细解释了 */     printf ("%f", d);     arg += sizeof(double);
 
    /* 打印第四个参数 */
    c = *(char *)arg;     printf ("%c", c);     arg += sizeof (char *);
 
    /* 打印第五个参数 */
    c = *(char *)arg;     printf ("%c", c);     arg += sizeof (char *);
 
    /* 打印第六个参数 */
    s = *(char **)arg;     printf ("%s", s);     arg += sizeof (char *);
 
    arg = (void *)0;           /* 使arg指针为 (void)0,
                                  实际上就上使无效,否则arg
                                  依然指向第六个参数,危险。*/
                               /* 相当于 va_end(arg) */
}
 
int main (void) {     print ("Hello/n", 3, 3.6, '/n', 'a', "World/n");
    return 0; }
/* File mytest.c ends here *******************/
 
 
代码有点长,其实很简单,只是机械地对main()函数中的print()函数中的6个参数进行处理,依次打印上面的6个参数(注意,我没有用格式化符号,带格式话符号的处理函数我将在下面给出),去掉注释,在Linux AS4.2 gcc-3.4.4中编译,运行结果如下:
 
/************************ Hello 33.600000 aWorld *************************/
 
与预想的完全一致。说明print()函数对变长参数的理解是正确的。
 
 
 
 
文章2:(一)写一个简单的可变参数的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_list arg_ptr;
  int j=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的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以  用ANSI C的定义就够了,也便于程序的移植.  小结:  可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实  现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必  要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多  态性来实现可变参数的功能,尽量避免用C语言的方式来实现. 
文章3:va_start用法(ZZ)

1:当无法列出传递函数的所有实参的类型和数目时,可用省略号指定参数表 void foo(...); void foo(parm_list,...);

2:函数参数的传递原理 函数参数是以数据结构:栈的形式存取,从右至左入栈.eg: #include <iostream> 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()结束。像这段代码: void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...) { va_list args; va_start(args, pszFormat); _vsnprintf(pszDest, DestLen, pszFormat, args); va_end(args); }

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

  1).演示如何使用参数个数可变的函数,采用ANSI标准形式   #include 〈stdio.h〉   #include 〈string.h〉   #include 〈stdarg.h〉   /*函数原型声明,至少需要一个确定的参数,注意括号内的省略号*/   int demo( char, ... );   void main( void )   {      demo("DEMO", "This", "is", "a", "demo!", "");   }   /*ANSI标准形式的声明方式,括号内的省略号表示可选参数*/   int demo( char msg, ... )   {        /*定义保存函数参数的结构*/      va_list argp;      int argno = 0;      char para;

     /*argp指向传入的第一个可选参数,msg是最后一个确定的参数*/      va_start( argp, msg );      while (1)        {       para = va_arg( argp, char);          if ( strcmp( para, "") == 0 )        break;          printf("Parameter #%d is: %s/n", argno, para);          argno++;    }    va_end( argp );    /*将argp置为NULL*/    return 0;   }

2)//示例代码1:可变参数函数的使用 #include "stdio.h" #include "stdarg.h" void simple_va_fun(int start, ...) {     va_list arg_ptr;    int nArgValue =start;     int nArgCout=0;     //可变参数的数目     va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。     do     {         ++nArgCout;         printf("the %d th arg: %d/n",nArgCout,nArgValue);     //输出各参数的值         nArgValue = va_arg(arg_ptr,int);                      //得到下一个可变参数的值     } while(nArgValue != -1);                    return; } int main(int argc, char* argv[]) {     simple_va_fun(100,-1);     simple_va_fun(100,200,-1);     return 0; }

3)//示例代码2:扩展——自己实现简单的可变参数的函数。 下面是一个简单的printf函数的实现,参考了<The C Programming Language>中的例子 #include "stdio.h" #include "stdlib.h" void myprintf(char* fmt, ...)        //一个简单的类似于printf的实现,//参数必须都是int 类型 {     char* pArg=NULL;               //等价于原来的va_list     char c;         pArg = (char*) &fmt;          //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值     pArg += sizeof(fmt);         //等价于原来的va_start              do     {         c =*fmt;         if (c != '%')         {             putchar(c);            //照原样输出字符         }         else         {            //按格式字符输出数据            switch(*++fmt)            {             case'd':                 printf("%d",*((int*)pArg));                           break;             case'x':                 printf("%#x",*((int*)pArg));                 break;             default:                 break;             }             pArg += sizeof(int);               //等价于原来的va_arg         }         ++fmt;     }while (*fmt != '/0');     pArg = NULL;                               //等价于va_end     return; } int main(int argc, char* argv[]) {     int i = 1234;     int j = 5678;         myprintf("the first test:i=%d/n",i,j);     myprintf("the secend test:i=%d; %x;j=%d;/n",i,0xabcd,j);     system("pause");     return 0; }

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Augusdi/article/details/6400240

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签