1. 程式人生 > >va_list、va_start、va_arg、va_end的原理與使用 (轉)

va_list、va_start、va_arg、va_end的原理與使用 (轉)

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