va_start(),va_end()函數應用

va_start(),va_end()函數應用

原理解釋:

VA_LIST 是在C語言中解決變參問題的一組宏,在<stdarg.h>頭文件下。

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 ap; //聲明一個變量來轉換參數列表   
va_start(ap,fmt);          //初始化變量   
va_end(ap);     //結束變量列表,和va_start成對使用   
可以根據va_arg(ap,type)取出參數  

已經經過調試成功的輸出程序

#include<stdio.h>
#include <stdarg.h>

#define bufsize 80
char buffer[bufsize];

int vspf(char *fmt, ...)
{
     va_list argptr;
     int cnt;
    va_start(argptr, fmt);

     cnt = vsnprintf(buffer,bufsize ,fmt, argptr);

     va_end(argptr);

    return(cnt);
}

int main(void)
{
     int inumber = 30;

     float fnumber = 90.0;

     char string[4] = "abc";

    vspf("%d %f %s", inumber, fnumber, string);

    printf("%s\n", buffer);

    return 0;
}

運行結果爲:

30 90.000000 abc

 

vsnprintf:int vsnprintf(char *str, size_t size, const char *format, va_list ap);
      write output to character sting str

      return value:the number of characters
       printed (not including the trailing '\0' used to end output to strings). The functions snprintf() and vsnprintf() do not write more than size bytes (including the trailing '\0'). If the output was truncated due to this limit then the return value is the number of characters (not including the trailing '\0') which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated. If an output error is encountered, a negative
value is returned.
                   if (return_value > -1)   
                       size = n+1;
                    else          
                       size *= 2;
The glibc implementation of the functions snprintf() and vsnprintf() conforms to the C99 standard, i.e., behaves as described above, since   glibc version 2.1. Until glibc 2.0.6 they would return -1 when the out put was truncated.
C語言用va_start等宏來處理這些可變參數。這些宏看起來很複雜,其實原理挺簡單,就是根據參

數入棧的特點從最靠近第一個可變參數的固定參數開始,依次獲取每個可變參數的地址。下面我們來分析這些宏。 在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)宏是爲了考慮那些內存地址需要對齊的系統,從宏的名字來應該是跟sizeof(int)對齊。一般的sizeof(int)=4,也就是參數在內存中的地址都爲4的倍數。比如,如果sizeof(n)在1-4之間,那麼_INTSIZEOF(n)=4;如果sizeof(n)在5-8之間,那麼_INTSIZEOF(n)=8。

  爲了能從固定參數依次得到每個可變參數,va_start,va_arg充分利用下面兩點:

  1. C語言在函數調用時,先將最後一個參數壓入棧

  2. X86平臺下的內存分配順序是從高地址內存到低地址內存

  高位地址

  第N個可變參數

  。。。

  第二個可變參數

  第一個可變參數      ? ap

  固定參數           ? v

  低位地址

  由上圖可見,v是固定參數在內存中的地址,在調用va_start後,ap指向第一個可變參數。這個宏的作用就是在v的內存地址上增加v所佔的內存大小,這樣就得到了第一個可變參數的地址。

  接下來,可以這樣設想,如果我能確定這個可變參數的類型,那麼我就知道了它佔用了多少內存,依葫蘆畫瓢,我就能得到下一個可變參數的地址。

  讓我再來看看va_arg,它先ap指向下一個可變參數,然後減去當前可變參數的大小即得到當前可變參數的內存地址,再做個類型轉換,返回它的值。

  要確定每個可變參數的類型,有兩種做法,要麼都是默認的類型,要麼就在固定參數中包含足夠的信息讓程序可以確定每個可變參數的類型。比如,printf,程序通過分析format字符串就可以確定每個可變參數大類型。

  最後一個宏就簡單了,va_end使得ap不再指向有效的內存地址。

  其實在varargs.h頭文件中定義了UNIX System V實行的va系列宏,而上面在stdarg.h頭文件中定義的是ANSI C形式的宏,這兩種宏是不兼容的,一般說來,我們應該使用ANSI C形式的va宏。

 

定義_INTSIZEOF(n)主要是爲了某些需要內存的對齊的系統.C語言的函數是從右向左壓入堆棧的,函數的參數在堆棧中的分佈位置.我
  們看到va_list被定義成char*,有一些平臺或操作系統定義爲void*.再看va_start的定義,定義爲&v+_INTSIZEOF(v),而&v是固定參數在堆棧的
  地址,所以我們運行va_start(ap, v)以後,ap指向第一個可變參數在堆棧的地址:
  高地址|-----------------------------|
  |函數返回地址 |
  |-----------------------------|
  |....... |
  |-----------------------------|
  |第n個參數(第一個可變參數) |
  |-----------------------------|<--va_start後ap指向
  |第n-1個參數(最後一個固定參數)|
  低地址|-----------------------------|<-- &v
  然後,我們用va_arg()取得類型t的可變參數值,以上例爲int型爲例,我們看一下va_arg取int型的返回值:
  j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) ); 首先ap+=sizeof(int),已經指向下一個參數的地址了.然後返回
  ap-sizeof(int)的int*指針,這正是第一個可變參數在堆棧裏的地址
  然後用*取得這個地址的內容(參數值)賦給j.
  高地址|-----------------------------|
  |函數返回地址 |
  |-----------------------------|
  |....... |
  |-----------------------------|<--va_arg後ap指向
  |第n個參數(第一個可變參數) |
  |-----------------------------|<--va_start後ap指向
  |第n-1個參數(最後一個固定參數)|
  低地址|-----------------------------|<-- &v
  最後要說的是va_end宏的意思,x86平臺定義爲ap=(char*)0;使ap不再指向堆棧,而是跟NULL一樣.有些直接定義爲((void*)0),這樣編譯器不
  會爲va_end產生代碼,例如gcc在linux的x86平臺就是這樣定義的.在這裏大家要注意一個問題:由於參數的地址用於va_start宏,所以參數不能聲明爲寄存器變量或作爲函數或數組類型.關於va_start, va_arg, va_end的描述就是這些了,我們要注意的是不同的操作系統和硬件平臺的定義有些不同,但原理卻是相似的.
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語言的方式來實現


va_start()va_end()函數應用:
man:
       #include <stdarg.h>

       void va_start(va_list ap, last);
       type va_arg(va_list ap, type);
       void va_end(va_list ap);
       void va_copy(va_list dest, va_list src);
1:當無法列出傳遞函數的所有實參的類型和數目時,可用省略號指定參數表
void foo(...);
void foo(parm_list,...);

2:函數參數的傳遞原理
函數參數是以數據結構:棧的形式存取,從右至左入棧.
eg:
#include   
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()結束。

4.va_start使argp指向第一個可選參數。va_arg返回參數列表中的當前參數並使argp指向參數列表中的下一個參數。va_end把argp指針清爲NULL。函數體內可以多次遍歷這些參數,但是都必須以va_start開始,並以va_end結尾。

實例:
編寫vstart.c,如下:

//vstart.c
#include <stdio.h>
#include <strings.h>
#include <stdarg.h>
int demo(char *fmt, ...);
int main()
{
   demo("DEMO", "This", "is", "a", "demo!", "");
   return 0;
}
int demo( char *fmt, ... )
{
   va_list argp;
   int argno = 0; 
   char *para;
   va_start(argp, fmt);
   while (1)
   {
      para = va_arg(argp, char *);
      if (strcmp( para, "") == 0)
      break;
      printf("Parameter #%d is: %s/n", argno, para);
      argno++;
   }
   va_end( argp );
   return 0;
}

  運行結果如下:

Parameter #0 is: This
Parameter #1 is: is
Parameter #2 is: a
Parameter #3 is: demo!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章