理解可變參數va_list、va_start、va_arg、va_end原理及使用方法

來源:互聯網
上載者:User

作者:陣利

轉自:小蜜蜂的專欄

 

  1. 概述
    由於在C語言中沒有函數重載,解決不定數目函數參數問題變得比較麻煩;即使採用C++,如果參數個數不能確定,也很難採用函數重載.對這種情況,有些人採用指標參數來解決問題.下面就c語言中處理不定參數數目的問題進行討論.
  2. 定義
    大家先看幾宏.
    在VC++6.0的include有一個stdarg.h標頭檔,有如下幾個宏定義:
    #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 )                            // 將指標置為無效
    如果對以上幾個宏定義不理解,可以略過,接這看後面的內容.
  3. 參數在堆棧中分布位置
    在進程中,堆棧地址是從高到低分配的.當執行一個函數的時候,將參數列表入棧,壓入堆棧的高地址部分,然後入棧函數的返回地址,接著入棧函數的執行代碼,這個入棧過程,堆棧地址不斷遞減,一些駭客就是在堆棧中修改函數返回地址,執行自己的代碼來達到執行自己插入的程式碼片段的目的.
    總之,函數在堆棧中的分布情況是:地址從高到低,依次是:函數參數列表,函數返回地址,函數執行程式碼片段.
    堆棧中,各個函數的分布情況是倒序的.即最後一個參數在列表中地址最高部分,第一個參數在列表地址的最低部分.參數在堆棧中的分布情況如下:
    最後一個參數
    倒數第二個參數
    ...
    第一個參數
    函數返回地址
    函數程式碼片段
  4. 範例程式碼
    void arg_test(int i, ...);
    int main(int argc,char *argv[])
    {
    int int_size = _INTSIZEOF(int);
    printf("int_size=%d/n", int_size);
    arg_test(0, 4);

    arg_cnt(4,1,2,3,4);
    return 0;
    }
    void arg_test(int i, ...)
    {
    int j=0;
    va_list arg_ptr;

    va_start(arg_ptr, i);
    printf("&i = %p/n", &i);//列印參數i在堆棧中的地址
    printf("arg_ptr = %p/n", arg_ptr);
    //列印va_start之後arg_ptr地址,
    //應該比參數i的地址高sizeof(int)個位元組
    //這時arg_ptr指向下一個參數的地址

    j=*((int *)arg_ptr);
    printf("%d %d/n", i, j);
    j=va_arg(arg_ptr, int);
    printf("arg_ptr = %p/n", arg_ptr);
    //列印va_arg後arg_ptr的地址
    //應該比調用va_arg前高sizeof(int)個位元組
    //這時arg_ptr指向下一個參數的地址
    va_end(arg_ptr);
    printf("%d %d/n", i, j);
    }

  5. 代碼說明:
    int int_size = _INTSIZEOF(int);得到int類型所佔位元組數
    va_start(arg_ptr, i); 得到第一個可變參數地址,根據定義(va_list)&v得到起始參數的地址, 再加上_INTSIZEOF(v) ,就是其實參數下一個參數的地址,即第一個可變參數地址.
    j=va_arg(arg_ptr, int); 得到第一個參參數的值,並且arg_ptr指標上移一個_INTSIZEOF(int),即指向下一個可變參數的地址.
    va_end(arg_ptr);置空arg_ptr,即arg_ptr=0;
    總結:讀取可變參數的過程其實就是堆棧中,使用指標,遍曆堆棧段中的參數列表,從低地址到高地址一個一個地把參數內容讀出來的過程.
  6. 在編程中應該注意的問題和解決辦法
    雖然可以通過在堆棧中遍曆參數列表來讀出所有的可變參數,但是由於不知道可變參數有多少個,什麼時候應該結束遍曆,如果在堆棧中遍曆太多,那麼很可能讀取一些無效的資料.
    解決辦法:a.可以在第一個起始參數中指定參數個數,那麼就可以在迴圈還中讀取所有的可變參數;b.定義一個結束標記,在調用函數的時候,在最後一個參數中傳遞這個標記,這樣在遍曆可變參數的時候,可以根據這個標記結束可變參數的遍曆;
    下面是一段範例程式碼:
    //第一個參數定義選擇性參數個數,用於迴圈取初參數內容
    void arg_cnt(int cnt, ...);
    int main(int argc,char *argv[])
    {
    int int_size = _INTSIZEOF(int);
    printf("int_size=%d/n", int_size);
    arg_cnt(4,1,2,3,4);
    return 0;
    }
    void arg_cnt(int cnt, ...)
    {
    int value=0;
    int i=0;
    int arg_cnt=cnt;
    va_list arg_ptr;
    va_start(arg_ptr, cnt);
    for(i = 0; i < cnt; i++)
    {
       value = va_arg(arg_ptr,int);
       printf("value%d=%d/n", i+1, value);
    }
    }

    雖然可以根據上面兩個辦法解決讀取參數個數的問題,但是如果參數類型都是不定的,該怎麼辦,如果不知道參數的類型,即使讀到了參數也沒有辦法進行處理.解決辦法:可以自訂一些可能出現的參數類型,這樣在可變參數列表中,可以可變參數列表中的那類型,然後根據類型,讀取可變參數值,並進行準確地轉換.傳遞參數的時候可以這樣傳遞:參數數目,可變參數類型1,可變參數值1,可變參數類型2,可變參數值2,....
    這裡給出一個完整的例子:
    #include <stdio.h>
    #include <stdarg.h>
    const int INT_TYPE   = 100000;
    const int STR_TYPE   = 100001;
    const int CHAR_TYPE   = 100002;
    const int LONG_TYPE   = 100003;
    const int FLOAT_TYPE = 100004;
    const int DOUBLE_TYPE = 100005;
    //第一個參數定義選擇性參數個數,用於迴圈取初參數內容
    //可變參數採用arg_type,arg_value...的形式傳遞,以處理不同的可變參數類型
    void arg_type(int cnt, ...);
    //第一個參數定義選擇性參數個數,用於迴圈取初參數內容
    void arg_cnt(int cnt, ...);
    //測試va_start,va_arg的使用方法,函數參數在堆棧中的地址分布情況
    void arg_test(int i, ...);
    int main(int argc,char *argv[])
    {
    int int_size = _INTSIZEOF(int);
    printf("int_size=%d/n", int_size);
    arg_test(0, 4);

    arg_cnt(4,1,2,3,4);
    arg_type(2, INT_TYPE, 222, STR_TYPE, "ok,hello world!");
    return 0;
    }
    void arg_test(int i, ...)
    {
    int j=0;
    va_list arg_ptr;

    va_start(arg_ptr, i);
    printf("&i = %p/n", &i);//列印參數i在堆棧中的地址
    printf("arg_ptr = %p/n", arg_ptr);
    //列印va_start之後arg_ptr地址,
    //應該比參數i的地址高sizeof(int)個位元組
    //這時arg_ptr指向下一個參數的地址

    j=*((int *)arg_ptr);
    printf("%d %d/n", i, j);
    j=va_arg(arg_ptr, int);
    printf("arg_ptr = %p/n", arg_ptr);
    //列印va_arg後arg_ptr的地址
    //應該比調用va_arg前高sizeof(int)個位元組
    //這時arg_ptr指向下一個參數的地址
    va_end(arg_ptr);
    printf("%d %d/n", i, j);
    }
    void arg_cnt(int cnt, ...)
    {
    int value=0;
    int i=0;
    int arg_cnt=cnt;
    va_list arg_ptr;
    va_start(arg_ptr, cnt);
    for(i = 0; i < cnt; i++)
    {
       value = va_arg(arg_ptr,int);
       printf("value%d=%d/n", i+1, value);
    }
    }
    void arg_type(int cnt, ...)
    {
    int arg_type = 0;
    int int_value=0;
    int i=0;
    int arg_cnt=cnt;
    char *str_value = NULL;
    va_list arg_ptr;
    va_start(arg_ptr, cnt);
    for(i = 0; i < cnt; i++)
    {
       arg_type = va_arg(arg_ptr,int);
       switch(arg_type)
       {
       case INT_TYPE:
        int_value = va_arg(arg_ptr,int);
        printf("value%d=%d/n", i+1, int_value);
        break;
       case STR_TYPE:
        str_value = va_arg(arg_ptr,char*);
        printf("value%d=%d/n", i+1, str_value);
        break;
       default:
        break;
       }
    }
    }

=======================================================================

有關VA_LIST的用法:

VA_LIST 是在C語言中解決變參問題的一組宏

VA_LIST的用法:     
       (1)首先在函數裡定義一具VA_LIST型的變數,這個變數是指向參數的指標
      (2)然後用VA_START宏初始設定變數剛定義的VA_LIST變數,這個宏的第二個參數是第一個可變參數的前一個參數,是一個固定的參數。
       (3)然後用VA_ARG返回可變的參數,VA_ARG的第二個參數是你要返回的參數的類型。
       (4)最後用VA_END宏結束可變參數的擷取。然後你就可以在函數裡使用第二個參數了。如果函數有多個可變參數的,依次調用VA_ARG擷取各個參數。

VA_LIST在編譯器中的處理:

(1)在運行VA_START(ap,v)以後,ap指向第一個可變參數在堆棧的地址。
(2)VA_ARG()取得類型t的可變參數值,在這步操作中首先apt = sizeof(t類型),讓ap指向下一個參數的地址。然後返回ap-sizeof(t類型)的t類型*指標,這正是第一個可變參數在堆棧裡的地址。然後用*取得這個地址的內容。
(3)VA_END(),X86平台定義為ap = ((char*)0),使ap不再指向堆棧,而是跟NULL一樣,有些直接定義為((void*)0),這樣編譯器不會為VA_END產生代碼,例如gcc在Linux的X86平台就是這樣定義的。

要注意的是:由於參數的地址用於VA_START宏,所以參數不能聲明為寄存器變數,或作為函數或數群組類型。

使用VA_LIST應該注意的問題:
   (1)因為va_start, va_arg, va_end等定義成宏,所以它顯得很愚蠢,可變參數的類型和個數完全在該函數中由程式碼控制,它並不能智能地識別不同參數的個數和類型. 也就是說,你想實現智能識別可變參數的話是要通過在自己的程式裡作判斷來實現的.
    (2)另外有一個問題,因為編譯器對可變參數的函數的原型檢查不夠嚴格,對編程查錯不利.不利於我們寫出高品質的代碼。
小結:可變參數的函數原理其實很簡單,而VA系列是以宏定義來定義的,實現跟堆棧相關。我們寫一個可變函數的C函數時,有利也有弊,所以在不必要的 場合,我們無需用到可變參數,如果在C++裡,我們應該利用C++多態性來實現可變參數的功能,盡量避免用C語言的方式來實現。

==========================================================================

變長參數應用舉例:

先得聲明一個變長參數的變數va_list list
在使用前要先用va_start(list, last_param)對list進行初始化,last_param為最右邊的已知參數,表示list
從last_param的下一個參數開始
va_arg(list, 類型)
最後不要忘了用va_end(list)

eg1:
#include<iostream>
#include<iomanip>
#include<stdarg.h>

using namespace std;

double average(int, ...);

int main()
{
    double w = 37.5, x = 22.5, y = 1.7, z = 10.2;

    cout << setiosflags(ios::fixed | ios::showpoint)
        << setprecision(1) << "w = " << w << "/nx = " << x
        << "/ny = " << y << "/nz = " << z << endl;

    cout << average(2, w, x) << endl;
    cout << average(3, w, x, y) << endl;
    cout << average(4, w, x, y, z) << endl;

    return 0;
}

double average(int i, ...)
{
    double total = 0;
    va_list ap;

    va_start(ap, i);

    for(int j = 1; j <= i; j++)
    {
        total += va_arg(ap, double);
    }

    va_end( ap );
    return total/i;
}

eg2:
#include<iostream.h>
#include <stdlib.h>
#include <stdarg.h>
void error(const char*format...);
void main()
{
    int a;
    char c='d';
    char s[100];
    error("Enter a string:");      //輸入一個字串
    cin>>s;
    error("Enter an integer:");    //輸入一整數
    cin>>a;
    error("%s/n%d/n%c/n",s,a,c);   //列印輸出

}
void error(const char*format...)    //實現像printf函數一樣的列印輸出功能
{
    int i;
    int j=0;
    va_list ap;
    va_start(ap,format);
    for(i=0;*(format+i)!=0;)
    {
        int in;
        char* pc;
        char d;
        if(*(format+i)=='%')
        {
            switch(*(format+i+1))
            {
            case'd':in=va_arg(ap,int);cout<<in;i=i+2;break;
            case's':pc=va_arg(ap,char*);cout<<pc;i=i+2;break;
            case'c':d=va_arg(ap,char);cout<<d;i=i+2;break;
            default:cout<<'%';i=i+1;break;
            }
        }
        else
        {
            cout<<*(format+i);
            i++;
        }

    }
}

================================================================

C++變長參數函數的用法

書上說,當無法列出傳遞函數的所有實參的類型和數目時,可用省略符號指定參數表
(...)

如:void foo(...);
     void foo(parm_list,...);
void foo(...)
{
    //...
}
調用:foo(a,b,c);

就是不懂,把a,b,c的值傳進函數裡面後,用什麼變數來接收???如果不能接收,(...)豈不是沒意義?
還有就是不明白
int printf(const char*...);
printf("hello,&s/n",userName);

這個c的輸出函數是怎麼用(...)實現的.

首先函數體中聲明一個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);
}

===========================================================

va_list的用法    
還記得printf函數調用的時候那個“...”嗎?就是可以輸入任意的參數。現在你用va_list也可以實作類別似的函式宣告,printf就是這樣做的。

va_list args;                                                 //聲明變數
va_start(args, before);                               //開始解析。args指向before後面的參數
參數類型 var = va_arg(args, 參數類型);     //取下一個參數並返回。args指向下一個參數
va_end(args);      

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.