va_start va_end 的使用和原理(转帖)

本文详细介绍了可变参数函数的工作原理及其在C语言中的实现方法,包括va_list、va_start、va_arg和va_end等宏的使用技巧,并讨论了可变参数函数在编程中的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

2:函数参数的传递原理
函数参数是以数据结构:栈的形式存取,从右至左入栈.eg:

先介绍一下可变参数表的调用形式以及原理:
首先是参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个开始入栈。因此栈底高地址,栈顶低地址,举个例子如下:
void func(int x, float y, char z);
那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在内存中变量的存放次序是 x->y->z,因此,从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。

下面是 <stdarg.h> 里面重要的几个宏定义如下:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。
例如 int max(int n, ...); 其函数内部应该如此实现:

 

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

int max(int n, ...) {                // 定参 n 表示后面变参数量,定界用,输入时切勿搞错
 va_list ap;                         // 定义一个 va_list 指针来访问参数表
     va_start(ap, n);                       // 初始化 ap,让它指向第一个变参,n之后的参数
    int maximum = -0x7FFFFFFF;          // 这是一个最小的整数
    int temp;
     for(int i = 0; i < n; i++) {
    temp = va_arg(ap, int);          // 获取一个 int 型参数,并且 ap 指向下一个参数
    if(maximum < temp) maximum = temp;
     }
    va_end(ap);                         // 善后工作,关闭 ap
    return max;
}
// 在主函数中测试 max 函数的行为(C++ 格式)
int main() {
   cout << max(3, 10, 20, 30) << endl;
   cout << max(6, 20, 40, 10, 50, 30, 40) << endl;
}
基本用法阐述至此,可以看到,这个方法存在两处极严重的漏洞:其一, 输入参数的类型随意性,使得参数很容易以一个不正确的类型获取一个值(譬如输入一个float,却以int型去获取他),这样做会出现莫名其妙的运行结果;其二, 变参表的大小并不能在运行时获取,这样就存在一个访问越界的可能性,导致后果严重的 RUNTIME ERROR。
来自: http://hi.baidu.com/yuji0228/blog/item/65f267eb8beaeedbd539c912.html
(一)写一个简单的可变参数的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语言的方式来实现. 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值