1. 程式人生 > >C/C++ 使用可變引數 & 原理

C/C++ 使用可變引數 & 原理

   可變引數

        VA函式(variable argument function),引數個數可變函式,又稱可變引數函式。

     格式說明:

        在C語言中,C標準函式庫的stdarg.h標頭檔定義了提供可變引數函式使用的巨集。在C++,應該使用標頭檔cstdarg。

        要建立一個可變引數函式,必須把省略號(...)放到引數列表後面。函式內部必須定義一個va_list變數。然後使用巨集va_start、va_arg和va_end來讀取。例如:

#include <stdarg.h>
 
double average(int count, ...)
{
    va_list ap;
    int j;
    double tot = 0;
    va_start(ap, count); //使va_list指向起始的引數
    for(j=0; j<count; j++)
        tot+=va_arg(ap, double); //檢索引數,必須按需要指定型別
    va_end(ap); //釋放va_list
    return tot/count;
}

        可變引數的函式原理其實很簡單,而va系列是以巨集定義來定義的,實現跟堆疊相關.我們寫一個可變函式的C函式時,有利也有弊,所以在不必要的場合,我們無需用到可變引數。如果在C++裡,我們應該利用C++的多型性來實現可變引數的功能,儘量避免用C語言的方式來實現。
        由於在C語言中沒有函式過載,解決不定數目函式引數問題變得比較麻煩,即使採用C++,如果引數個數不能確定,也很難採用函式過載。對這種情況,提出了指標引數來解決問題。        

// printf()函式,其原型為:
int   printf(   const   char*   format,   ...);
//它除了有一個引數format固定以外,後面跟的引數的個數和型別是可變的,例如我們可以有以下不同的呼叫方法:   
printf( "%d ",i);   
printf( "%s ",s);   
printf( "the   number   is   %d   ,string   is:%s ",   i,   s);  
          我們需要以下幾個巨集定義:
(1)va_list
定義了一個指標arg_ptr, 用於指示可選的引數.
(2)va_start(arg_ptr, argN)
使引數列表指標arg_ptr指向函式引數列表中的第一個可選引數,argN是位於第一個可選引數之前的固定引數, 或者說最後一個固定引數.如有一va函式的宣告是void va_test(char a, char b, char c, ...), 則它的固定引數依次是a,b,c, 最後一個固定引數argN為c, 因此就是va_start(arg_ptr, c).
(3)va_arg(arg_ptr, type)
返回引數列表中指標arg_ptr所指的引數, 返回型別為type. 並使指標arg_ptr指向引數列表中下一個引數.返回的是可選引數, 不包括固定引數.
(4)va_end(arg_ptr)

清空引數列表, 並置引數指標arg_ptr無效.
(注:va在這裡是variable-argument(可變引數)的意思. 這些巨集定義在stdarg.h中,所以用到可變引數的程式應該包含這個標頭檔案)。例:
#include <stdarg.h>
int main(int argc,char *argv[])
{
    simple_va_fun(100);   
    simple_va_fun(100,200);
    simple_va_fun(100,200,'a');
    return 0;
}
void simple_va_fun(int i,...)   
{   
    va_list   arg_ptr;   //定義可變引數指標 
    va_start(arg_ptr,i);   // i為最後一個固定引數
    int j=va_arg(arg_ptr,int);   //返回第一個可變引數,型別為int
    char c=va_arg(arg_ptr,char);   //返回第二個可變引數,型別為char
    va_end(arg_ptr);        //  清空引數指標
    printf( "%d %d %c\n",i,j,c);   
    return;   
}

      實現原理

        C函式呼叫的棧結構:
        可變引數函式的實現與函式呼叫的棧結構密切相關,正常情況下C的函式引數入棧規則為__stdcall, 它是從右到左的,即函式中的最右邊的引數最先入棧。例如,對於函式:

void fun(int a, int b, int c)
{
int d;
...
}
       其棧結構為
0x1ffc-->d
0x2000-->a
0x2004-->b
0x2008-->c
      對於在32位系統的多數編譯器,每個棧單元的大小都是sizeof(int), 而函式的每個引數都至少要佔一個棧單元大小,如函式 void fun1(char a, int b, double c, short d) 對一個32的系統其棧的結構就是
0x1ffc-->a (4位元組)
0x2000-->b (4位元組)
0x2004-->c (8位元組)
0x200c-->d (4位元組)
       因此,函式的所有引數是儲存線上性連續的棧空間中的,基於這種儲存結構,這樣就可以從可變引數函式中必須有的第一個普通引數來定址後續的所有可變引數的型別及其值

        先看看固定引數列表函式:

void fixed_args_func(int a, double b, char *c)
{
printf("a = 0x%p\n", &a);
printf("b = 0x%p\n", &b);
printf("c = 0x%p\n", &c);
}
       對於固定引數列表的函式,每個引數的名稱、型別都是直接可見的,他們的地址也都是可以直接得到的,比如:通過&a我們可以得到a的地址,並通過函式原型聲明瞭解到a是int型別的。
       但是對於變長引數的函式,我們就沒有這麼順利了。還好,按照C標準的說明,支援變長引數的函式在原型宣告中,必須有至少一個最左固定引數(這一點與傳統C有區別,傳統C允許不帶任何固定引數的純變長引數函式),這樣我們可以得到其中固定引數的地址,但是依然無法從宣告中得到其他變長引數的地址,比如:
void var_args_func(const char * fmt, ...) 
{
... ... 
}
         這裡我們只能得到fmt這固定引數的地址,僅從函式原型我們是無法確定"..."中有幾個引數、引數都是什麼型別的。回想一下函式傳參的過程,無論"..."中有多少個引數、每個引數是什麼型別的,它們都和固定引數的傳參過程是一樣的,簡單來講都是棧操作,而棧這個東西對我們是開放的。這樣一來,一旦我們知道某函式幀的棧上的一個固定引數的位置,我們完全有可能推匯出其他變長引數的位置。

         我們先用上面的那個fixed_args_func函式確定一下入棧順序。

int main() 
{
fixed_args_func(17, 5.40, "hello world");
return 0;
}
a = 0x0022FF50
b = 0x0022FF54
c = 0x0022FF5C
          從這個結果來看,顯然引數是從右到左,逐一壓入棧中的(棧的延伸方向是從高地址到低地址,棧底的佔領著最高記憶體地址,先入棧的引數,其地理位置也就最高了)。
         我們基本可以得出這樣一個結論:
c.addr = b.addr + x_sizeof(b); /*注意: x_sizeof !=sizeof */
b.addr = a.addr + x_sizeof(a);
         有了以上的"等式",我們似乎可以推匯出 void var_args_func(const char * fmt, ... ) 函式中,可變引數的位置了。起碼第一個可變引數的位置應該是:first_vararg.addr = fmt.addr + x_sizeof(fmt); 根據這一結論我們試著實現一個支援可變引數的函式:
#include <stdarg.h>
#include <stdio.h>

void var_args_func(const char * fmt, ...) 
{
char *ap;

ap = ((char*)&fmt) + sizeof(fmt);
printf("%d\n", *(int*)ap); 

ap = ap + sizeof(int);
printf("%d\n", *(int*)ap);

ap = ap + sizeof(int);
printf("%s\n", *((char**)ap));
}

int main()
{
var_args_func("%d %d %s\n", 4, 5, "hello world");
   return 0;
}
期待輸出結果:
4
5
hello world
         先來解釋一下這個程式。我們用ap獲取第一個變參的地址,我們知道第一個變參是4,一個int 型,所以我們用(int*)ap以告訴編譯器,以ap為首地址的那塊記憶體我們要將之視為一個整型來使用,*(int*)ap獲得該引數的值;接下來的變參是5,又一個int型,其地址是ap + sizeof(第一個變參),也就是ap + sizeof(int),同樣我們使用*(int*)ap獲得該引數的值;最後的一個引數是一個字串,也就是char*,與前兩個int型引數不同的是,經過ap + sizeof(int)後,ap指向棧上一個char*型別的記憶體塊(我們暫且稱之tmp_ptr, char *tmp_ptr)的首地址,即ap -> &tmp_ptr,而我們要輸出的不是printf("%s\n", ap),而是printf("%s\n", tmp_ptr); printf("%s\n", ap)是意圖將ap所指的記憶體塊作為字串輸出了,但是ap -> &tmp_ptr,tmp_ptr所佔據的4個位元組顯然不是字串,而是一個地址。如何讓&tmp_ptr是char **型別的,我們將ap進行強制轉換(char**)ap <=> &tmp_ptr,這樣我們訪問tmp_ptr只需要在(char**)ap前面加上一個*即可,即printf("%s\n", *(char**)ap);

         一切似乎很完美,編譯也很順利通過,但執行上面的程式碼後,不但得不到預期的結果,反而整個編譯器會強行關閉(大家可以嘗試著執行一下),原來是ap指標在後來並沒有按照預期的要求指向第二個變引數,即並沒有指向5所在的首地址,而是指向了未知記憶體區域,所以編譯器會強行關閉。其實錯誤開始於:ap = ap + sizeof(int);由於記憶體對齊,編譯器在棧上壓入引數時,不是一個緊挨著另一個的,編譯器會根據變參的型別將其放到滿足型別對齊的地址上的,這樣棧上引數之間實際上可能會是有空隙的。(參考(原文)C語言記憶體對齊相關文章)所以此時的ap計算應該改為:ap = (char *)ap +sizeof(int) + __va_rounded_size(int);

#include<stdio.h>

#define __va_rounded_size(TYPE) \
(((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

void var_args_func(const char * fmt, ...) 
{
char *ap;

ap = ((char*)&fmt) + sizeof(fmt);
printf("%d\n", *(int*)ap); 

ap = (char *)ap + sizeof(int) + __va_rounded_size(int);
printf("%d\n", *(int*)ap);

ap = ap + sizeof(int) + __va_rounded_size(int);
printf("%s\n", *((char**)ap));
}

int main()
{
var_args_func("%d %d %s\n", 4, 5, "hello world"); 
return 0;
}
         var_args_func只是為了演示,並未根據fmt訊息中的格式字串來判斷變參的個數和型別,而是直接在實現中寫死了。
         為了滿足程式碼的可移植性,C標準庫在stdarg.h中提供了諸多便利以供實現變長長度引數時使用。這裡也列出一個簡單的例子,看看利用標準庫是如何支援變長引數的:
#include <stdarg.h>#include <stdio.h>
void std_vararg_func(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);

printf("%d\n", va_arg(ap, int));
printf("%f\n", va_arg(ap, double));
printf("%s\n", va_arg(ap, char*));

va_end(ap);
} 
int main() {
std_vararg_func("%d %f %s\n", 4, 5.4, "hello world"); return 0;}
          對比一下 std_vararg_func和var_args_func的實現,va_list似乎就是char*, va_start似乎就是 ((char*)&fmt) + sizeof(fmt),va_arg似乎就是得到下一個引數的首地址。沒錯,多數平臺下stdarg.h中va_list, va_start和var_arg的實現就是類似這樣的。一般stdarg.h會包含很多巨集,看起來比較複雜。
         在《C程式設計語言》中,Ritchie提供了一個簡易版printf函式:
#include<stdarg.h>

void minprintf(char *fmt, ...)
{
    va_list ap;
    char *p, *sval;
    int ival;
    double dval;

    va_start(ap, fmt);
    for (p = fmt; *p; p++) {
        if(*p != '%') {
            putchar(*p);
            continue;
        }
        switch(*++p) {
        case 'd':
            ival = va_arg(ap, int);
            printf("%d", ival);
            break;
        case 'f':
            dval = va_arg(ap, double);
            printf("%f", dval);
            break;
        case 's':
            for (sval = va_arg(ap, char *); *sval; sval++)
                putchar(*sval);
            break;
        default:
            putchar(*p);
            break;
        }
    }
    va_end(ap);
}
------------------------我是分割線------------------------

參考:

--http://www.jb51.net/article/41868.htm 《C/C++中可變引數的詳細介紹》

--http://www.cnblogs.com/cpoint/p/3368993.html 《C語言中可變引數函式實現原理》