1. 程式人生 > >va_start(),va_arg(),va_end()

va_start(),va_arg(),va_end()

(一)寫一個簡單的可變引數的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;
}