C 語言的可變參數表函數的設計

首先在介紹可變參數表函數的設計之前,我們先來介紹一下最經典的可變參數表printf函數的實現原理。
一、printf函數的實現原理
在C/C++中,對函數參數的掃描是從後向前的。C/C++的函數參數是通過壓入堆棧的方式來給函數傳參數的(堆棧是一種先進後出的數據結構),最先壓入的參數最後出來,在計算機的內存中,數據有2塊,一塊是堆,一塊是棧(函數參數及局部變量在這裏),而棧是從內存的高地址向低地址生長的,控制生長的就是堆棧指針了,最先壓入的參數是在最上面,就是說在所有參數的最後面,最後壓入的參數在最下面,結構上看起來是第一個,所以最後壓入的參數總是能夠被函數找到,因爲它就在堆棧指針的上方。printf的第一個被找到的參數就是那個字符指針,就是被雙引號括起來的那一部分,函數通過判斷字符串裏控制參數的個數來判斷參數個數及數據類型,通過這些就可算出數據需要的堆棧指針的偏移量了,下面給出printf("%d,%d",a,b);(其中a、b都是int型的)的彙編代碼
  1. .section  
  2. .data  
  3. string out = "%d,%d"  
  4. push b  
  5. push a  
  6. push $out  
  7. call printf  
你會看到,參數是最後的先壓入棧中,最先的後壓入棧中,參數控制的那個字符串常量是最後被壓入的,所以這個常量總是能被找到的。
二、可變參數表函數的設計
      標準庫提供的一些參數的數目可以有變化的函數。例如我們很熟悉的printf,它需要有一個格式串,還應根據需要爲它提供任意多個“其他參數”。這種函數被稱作“具有變長度參數表的函數”,或簡稱爲“變參數函數”。我們寫程序中有時也可能需要定義這種函數。要定義這類函數,就必須使用標準頭文件<stdarg.h>,使用該文件提供的一套機制,並需要按照規定的定義方式工作。本節介紹這個頭文件提供的有關功能,它們的意義和使用,並用例子說明這類函數的定義方法。
      C中變長實參頭文件stdarg.h提供了一個數據類型va-list和三個宏(va-start、va-arg和va-end),用它們在被調用函數不知道參數個數和類型時對可變參數表進行測試,從而爲訪問可變參數提供了方便且有效的方法。va-list是一個char類型的指針,當被調用函數使用一個可變參數時,它聲明一個類型爲va-list的變量,該變量用來指向va-arg和va-end所需信息的位置。下面給出va_list在C中的源碼:
  1. typedef char *  va_list;  
     void va-start(va-list ap,lastfix)是一個宏,它使va-list類型變量ap指向被傳遞給函數的可變參數表中的第一個參數,在第一次調用va-arg和va-end之前,必須首先調用該宏。va-start的第二個參數lastfix是傳遞給被調用函數的最後一個固定參數的標識符。va-start使ap只指向lastfix之外的可變參數表中的第一個參數,很明顯它先得到第一個參數內存地址,然後又加上這個參數的內存大小,就是下個參數的內存地址了。下面給出va_start在C中的源碼:
  1. #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  
  2. #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可變參數中第一個參數的首地址  
      type va-arg(va-list ap,type)也是一個宏,其使用有雙重目的,第一個是返回ap所指對象的值,第二個是修改參數指針ap使其增加以指向表中下一個參數。va-arg的第二個參數提供了修改參數指針所必需的信息。在第一次使用va-arg時,它返回可變參數表中的第一個參數,後續的調用都返回表中的下一個參數,下面給出va_arg在C中的源碼:
  1. #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //將參數轉換成需要的類型,並使ap指向下一個參數  
     在使用va-arg時,要注意第二個參數所用類型名應與傳遞到堆棧的參數的字節數對應,以保證能對不同類型的可變參數進行正確地尋址,比如實參依次爲char型、char * 型、int型和float型時,在va-arg中它們的類型則應分別爲int、char *、int和double.
     void va-end(va-list ap)也是一個宏,該宏用於被調用函數完成正常返回,功能就是把指針ap賦值爲0,使它不指向內存的變量。下面給出va_end在C中的源碼:

  1. #define va_end(ap)      ( ap = (va_list)0 )  
     va-end必須在va-arg讀完所有參數後再調用,否則會產生意想不到的後果。特別地,當可變參數表函數在程序執行過程中不止一次被調用時,在函數體每次處理完可變參數表之後必須調用一次va-end,以保證正確地恢復棧。
    一個變參數函數至少需要有一個普通參數,其普通參數可以具有任何類型。在函數定義中,這種函數的最後一個普通參數除了一般的用途之外,還有其他特殊用途。下面從一個例子開始說明有關的問題。
假設我們想定義一個函數sum,它可以用任意多個整數類型的表達式作爲參數進行調用,希望sum能求出這些參數的和。這時我們應該將sum定義爲一個只有一個普通參數,並具有變長度參數表的函數,這個函數的頭部應該是(函數原型與此類似):
int sum(int n, ...)

我們實際上要求在函數調用時,從第一個參數n得到被求和的表達式個數,從其餘參數得到被求和的表達式。在參數表最後連續寫三個圓點符號,說明這個函數具有可變數目的參數。凡參數表具有這種形式(最後寫三個圓點),就表示定義的是一個變參數函數。注意,這樣的三個圓點只能放在參數表最後,在所有普通參數之後。
下面假設函數sum裏所用的va_list類型的變量的名字是vap。在能夠用vap訪問實際參數之前,必須首先用宏a_start對這個變量進行初始化。宏va_start的類型特徵可以大致描述爲:

va_start(va_list vap, 最後一個普通參數)
在函數sum裏對vap初始化的語句應當寫爲:

va_start(vap, n); 相當於  char *vap= (char *)&n + sizeof(int);
此時vap正好指向n後面的可變參數表中的第一個參數。

在完成這個初始化之後,我們就可以通過另一個宏va_arg訪問函數調用的各個實際參數了。宏va_arg的類型特徵可以大致地描述爲:
類型 va_arg(va_list vap, 類型名)
在調用宏va_arg時必須提供有關實參的實際類型,這一類型也將成爲這個宏調用的返回值類型。對va_arg的調用不僅返回了一個實際參數的值(“當前”實際參數的值),同時還完成了某種更新操作,使對這個宏va_arg的下次調用能得到下一個實際參數。對於我們的例子,其中對宏va_arg的一次調用應當寫爲:
v = va_arg(vap, int);
這裏假定v是一個有定義的int類型變量。
在變參數函數的定義裏,函數退出之前必須做一次結束動作。這個動作通過對局部的va_list變量調用宏va_end完成。這個宏的類型特徵大致是:
void va_end(va_list vap);
三、棧中參數分佈以及宏使用後的指針變化說明如下:

下面是函數sum的完整定義,從中可以看到各有關部分的寫法:

  1. #include<iostream>  
  2. using namespace std;  
  3. #include<stdarg.h>  
  4.   
  5. int sum(int n,...)  
  6. {  
  7.     int i , sum = 0;  
  8.     va_list vap;  
  9.     va_start(vap , n);     //指向可變參數表中的第一個參數  
  10.     for(i = 0 ; i < n ; ++i)  
  11.         sum += va_arg(vap , int);     //取出可變參數表中的參數,並修改參數指針vap使其增加以指向表中下一個參數  
  12.     va_end(vap);    //把指針vap賦值爲0  
  13.     return sum;  
  14. }  
  15. int main(void)  
  16. {  
  17.     int m = sum(3 , 45 , 89 , 72);  
  18.     cout<<m<<endl;  
  19.     return 0;  
  20. }  
這裏首先定義了va_list變量vap,而後對它初始化。循環中通過va_arg取得順序的各個實參的值,並將它們加入總和。最後調用va_end結束。
下面是調用這個函數的幾個例子:
k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);
函數sum中首先定義了可變參數表指針vap,而後通過va_start ( vap, n )取得了參數表首地址(賦值給了vap),其後的for循環則用來遍歷可變參數表。這種遍歷方式與我們在數據結構教材中經常看到的遍歷方式是類似的。
  函數sum看起來簡潔明瞭,但是實際上printf的實現卻遠比這複雜。sum函數之所以看起來簡單,是因爲:
  1、sum函數可變參數表的長度是已知的,通過num參數傳入;
  2、sum函數可變參數表中參數的類型是已知的,都爲int型。
  而printf函數則沒有這麼幸運。首先,printf函數可變參數的個數不能輕易的得到,而可變參數的類型也不是固定的,需由格式字符串進行識別(由%f、%d、%s等確定),因此則涉及到可變參數表的更復雜應用。
在這個函數中,需通過對傳入的格式字符串(首地址爲lpStr)進行識別來獲知可變參數個數及各個可變參數的類型,具體實現體現在for循環中。譬如,在識別爲%d後,做的是va_arg ( vap, int ),而獲知爲%l和%lf後則進行的是va_arg ( vap, long )、va_arg ( vap, double )。格式字符串識別完成後,可變參數也就處理完了。

在編寫和使用具有可變數目參數的函數時,有幾個問題值得注意。
第一:調用va_arg將更新被操作的va_list變量(如在上例的vap),使下次調用可以得到下一個參數。在執行這個操作時,va_arg並不知道實際有幾個參數,也不知道參數的實際類型,它只是按給定的類型完成工作。因此,寫程序的人應在變參數函數的定義裏注意控制對實際參數的處理過程。上例通過參數n提供了參數個數的信息,就是爲了控制循環。標準庫函數printf根據格式串中的轉換描述的數目確定實際參數的個數。如果這方面信息有誤,函數執行中就可能出現嚴重問題。編譯程序無法檢查這裏的數據一致性問題,需要寫程序的人自己負責。在前面章節裏,我們一直強調對printf等函數調用時,要注意格式串與其他參數個數之間一致性,其原因就在這裏。
第二:編譯系統無法對變參數函數中由三個圓點代表的那些實際參數做類型檢查,因爲函數的頭部沒有給出這些參數的類型信息。因此編譯處理中既不會生成必要的類型轉換,也不會提供類型錯誤信息。考慮標準庫函數printf,在調用這個函數時,不但實際參數個數可能變化,各參數的類型也可能不同,因此不可能有統一方式來描述它們的類型。對於這種參數,C語言的處理方式就是不做類型檢查,要求寫程序的人保證函數調用的正確性。
假設我們寫出下面的函數調用:
k = sum(6, 2.4, 4, 5.72, 6, 2);

編譯程序不會發現這裏參數類型不對,需要做類型轉換,所有實參都將直接傳給函數。函數裏也會按照內部定義的方式把參數都當作整數使用。編譯程序也不會發現參數個數與6不符。這一調用的結果完全由編譯程序和執行環境決定,得到的結果肯定不會是正確的。

四、簡單的練習

問題1:可變長參數的獲取
  有這樣一個具有可變長參數的函數,其中有下列代碼用來獲取類型爲float的實參:
  va_arg (argp, float);
  這樣做可以嗎?
  答案與分析:
  不可以。在可變長參數中,應用的是"加寬"原則。也就是float類型被擴展成double;char、 short類型被擴展成int。因此,如果你要去可變長參數列表中原來爲float類型的參數,需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。
  問題2:定義可變長參數的一個限制
  爲什麼我的編譯器不允許我定義如下的函數,也就是可變長參數,但是沒有任何的固定參數?
  1. int f(...)  
  2. {  
  3.     ......  
  4.     ......  
  5.     ......  
  6. }  
答案與分析:
  不可以。這是ANSI C 所要求的,你至少得定義一個固定參數。這個參數將被傳遞給va_start(),然後用va_arg()和va_end()來確定所有實際調用時可變長參數的類型和值。
      問題3:如何判別可變參數函數的參數類型?
函數形式如下:
  1. void fun(char *str ,...)  
  2. {  
  3.     ......  
  4.     ......  
  5.     ......  
  6. }  
若傳的參數個數大於1,如何判別第2個以後傳參的參數類型???
答案與分析:
這個是沒有辦法判斷的,例如printf( "%d%c%s ",   ....)是通過格式串中的%d、 %c、 %s來確定後面參數的類型,其實你也可以參考這種方法來判斷不定參數的類型。
最後,奉獻上自己寫的一個printf函數
  1. #include<stdio.h>  
  2. #include<stdarg.h>  
  3.   
  4. void myitoa(int n, char str[], int radix)  
  5. {  
  6.     int i , j , remain;  
  7.     char tmp;  
  8.     i = 0;  
  9.     do  
  10.     {  
  11.         remain = n % radix;  
  12.         if(remain > 9)  
  13.             str[i] = remain  - 10 + 'A';  
  14.         else  
  15.             str[i] = remain + '0';  
  16.         i++;  
  17.     }while(n /= radix);  
  18.     str[i] = '\0';  
  19.   
  20.     for(i-- , j = 0 ; j <= i ; j++ , i--)  
  21.     {  
  22.         tmp = str[j];  
  23.         str[j] = str[i];  
  24.         str[i] = tmp;  
  25.     }  
  26.   
  27. }  
  28.   
  29. void myprintf(const char *format, ...)  
  30. {  
  31.     char c, ch, str[30];  
  32.     va_list ap;  
  33.   
  34.     va_start(ap, format);  
  35.     while((c = *format))  
  36.     {  
  37.         switch(c)  
  38.         {  
  39.         case '%':  
  40.             ch = *++format;  
  41.             switch(ch)  
  42.             {  
  43.             case 'd':  
  44.                 {  
  45.                     int n = va_arg(ap, int);  
  46.                     myitoa(n, str, 10);  
  47.                     fputs(str, stdout);  
  48.                     break;  
  49.                 }  
  50.             case 'x':  
  51.                 {  
  52.                     int n = va_arg(ap, int);  
  53.                     myitoa(n, str, 16);  
  54.                     fputs(str, stdout);  
  55.                     break;  
  56.                 }  
  57.             case 'f':  
  58.                 {  
  59.                     double f = va_arg(ap, double);  
  60.                     int n;  
  61.                     n = f;  
  62.                     myitoa(n, str, 10);  
  63.                     fputs(str, stdout);  
  64.                     putchar('.');  
  65.                     n = (f - n) * 1000000;  
  66.                     myitoa(n, str, 10);  
  67.                     fputs(str, stdout);  
  68.                     break;  
  69.                 }  
  70.             case 'c':  
  71.                 {  
  72.                     putchar(va_arg(ap, int));  
  73.                     break;  
  74.                 }  
  75.             case 's':  
  76.                 {  
  77.                     char *p = va_arg(ap, char *);  
  78.                     fputs(p, stdout);  
  79.                     break;  
  80.                 }  
  81.             case '%':  
  82.                 {  
  83.                     putchar('%');  
  84.                     break;  
  85.                 }  
  86.             default:  
  87.                 {  
  88.                     fputs("format invalid!", stdout);  
  89.                     break;  
  90.                 }  
  91.             }  
  92.             break;  
  93.         default:  
  94.             putchar(c);  
  95.             break;  
  96.         }  
  97.         format++;  
  98.     }  
  99.     va_end(ap);  
  100. }  
  101.   
  102. int main(void)  
  103. {  
  104.     myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B'"hello");  
  105.     return 0;  
  106. }  

轉自:Hackbuteer1的專欄

原帖地址:http://blog.csdn.net/hackbuteer1/article/details/7558979#

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