va_list、va_start和va_end使用


我們知道va_start,va_arg,va_end是在stdarg.h中被定義成宏的,由於1、硬件平台的不同 2、編譯器的不同,所以定義的宏也有所不同。

在ANSI C中,這些宏的定義位於stdarg.h中,典型的實現如下:

typedef char *va_list;

va_start宏,獲取可變參數列表的第一個參數的地址(list是類型為va_list的指針,param1是可變參數最左邊的參數):

#define va_start(list,param1)   ( list = (va_list)&param1+ sizeof(param1) )

va_arg宏,獲取可變參數的當前參數,返回指定類型並將指針指向下一參數(mode參數描述了當前參數的類型):

#define va_arg(list,mode)   ( (mode *) ( list += sizeof(mode) ) )[-1]

va_end宏,清空va_list可變參數列表:

#define va_end(list) ( list = (va_list)0 )

 注:以上sizeof()只是為了說明工作原理,實際實現中,增加的字節數需保證為為int的整數倍

如:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

 

為了理解這些宏的作用,我們必須先搞清楚:C語言中函數參數的內存布局。首先,函數參數是存儲在棧中的,函數參數從右往左依次入棧。

以下面函數為討論對象:

復制代碼
void test(char *para1,char *param2,char *param3, char *param4)
{
      va_list list;
      ......
      return;
}
復制代碼

在linux中,棧由高地址往低地址生長,調用test函數時,其參數入棧情況如下:

 

 當調用va_start(list,param1) 時:list指針指向情況對應下圖:

 

最復雜的宏是va_arg。它必須返回一個由va_list所指向的恰當的類型的數值,同時遞增va_list,使它指向參數列表中的一個參數(即遞增的大小等於與va_arg宏所返回的數值具有相同類型的對象的長度)。因為類型轉換的結果不能作為賦值運算的目標,所以va_arg宏首先使用sizeof來確定需要遞增的大小,然后把它直接加到va_list上,這樣得到的指針再被轉換為要求的類型。因為該指針現在指向的位置"過"了一個類型單位的大小,所以我們使用了下標-1來存取正確的返回參數。

 

原地址:https://www.cnblogs.com/bettercoder/p/3488299.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的定義就夠了,也便於程序的移植.

 

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, ...); 其函數內部應該如此實現:

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。

#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:: 



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

 

原地址:https://blog.csdn.net/holandstone/article/details/8268732


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM