1. 程式人生 > >C 語言的可變引數表函式的設計

C 語言的可變引數表函式的設計

                首先在介紹可變引數表函式的設計之前,我們先來介紹一下最經典的可變引數表printf函式的實現原理。一、printf函式的實現原理在C/C++中,對函式引數的掃描是從後向前的。C/C++的函式引數是通過壓入堆疊的方式來給函式傳引數的(堆疊是一種先進後出的資料結構),最先壓入的引數最後出來,在計算機的記憶體中,資料有2塊,一塊是堆,一塊是棧(函式引數及區域性變數在這裡),而棧是從記憶體的高地址向低地址生長的,控制生長的就是堆疊指標了,最先壓入的引數是在最上面,就是說在所有引數的最後面,最後壓入的引數在最下面,結構上看起來是第一個,所以最後壓入的引數總是能夠被函式找到,因為它就在堆疊指標的上方。printf的第一個被找到的引數就是那個字元指標,就是被雙引號括起來的那一部分,函式通過判斷字串裡控制引數的個數來判斷引數個數及資料型別
,通過這些就可算出資料需要的堆疊指標的偏移量了,下面給出printf("%d,%d",a,b);(其中a、b都是int型的)的彙編程式碼
.section.datastring out = "%d,%d"push bpush apush $outcall 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中的原始碼:
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中的原始碼:
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#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中的原始碼:
#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中的原始碼:
#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的完整定義,從中可以看到各有關部分的寫法:
#include<iostream>using namespace std;#include<stdarg.h>int sum(int n,...)int i , sum = 0; va_list vap; va_start(vap , n);     //指向可變引數表中的第一個引數 for(i = 0 ; i < n ; ++i)  sum += va_arg(vap , int);     //取出可變引數表中的引數,並修改引數指標vap使其增加以指向表中下一個引數 va_end(vap);    //把指標vap賦值為0 return sum;}int main(void)int m = sum(3 , 45 , 89 , 72); cout<<m<<endlreturn 0;}
這裡首先定義了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:定義可變長引數的一個限制  為什麼我的編譯器不允許我定義如下的函式,也就是可變長引數,但是沒有任何的固定引數?
int f(...){ ...... ...... ......}
答案與分析:  不可以。這是ANSI C 所要求的,你至少得定義一個固定引數。這個引數將被傳遞給va_start(),然後用va_arg()和va_end()來確定所有實際呼叫時可變長引數的型別和值。      問題3:如何判別可變引數函式的引數型別?函式形式如下:
void fun(char *str ,...){ ...... ...... ......}
若傳的引數個數大於1,如何判別第2個以後傳參的引數型別???答案與分析:這個是沒有辦法判斷的,例如printf( "%d%c%s ",   ....)是通過格式串中的%d、 %c、 %s來確定後面引數的型別,其實你也可以參考這種方法來判斷不定引數的型別。最後,奉獻上自己寫的一個printf函式
#include<stdio.h>#include<stdarg.h>void myitoa(int n, char str[], int radix)int i , j , remain; char tmp; i = 0do {  remain = n % radix;  if(remain > 9)   str[i] = remain  - 10 + 'A';  else   str[i] = remain + '0';  i++; }while(n /= radix); str[i] = '\0'for(i-- , j = 0 ; j <= i ; j++ , i--) {  tmp = str[j];  str[j] = str[i];  str[i] = tmp; }}void myprintf(const char *format, ...)char c, ch, str[30]; va_list ap; va_start(ap, format); while((c = *format)) {  switch(c)  {  case '%':   ch = *++format;   switch(ch)   {   case 'd':    {     int n = va_arg(ap, int);     myitoa(n, str, 10);     fputs(str, stdout);     break;    }   case 'x':    {     int n = va_arg(ap, int);     myitoa(n, str, 16);     fputs(str, stdout);     break;    }   case 'f':    {     double f = va_arg(ap, double);     int n;     n = f;     myitoa(n, str, 10);     fputs(str, stdout);     putchar('.');     n = (f - n) * 1000000;     myitoa(n, str, 10);     fputs(str, stdout);     break;    }   case 'c':    {     putchar(va_arg(ap, int));     break;    }   case 's':    {     char *p = va_arg(ap, char *);     fputs(p, stdout);     break;    }   case '%':    {     putchar('%');     break;    }   default:    {     fputs("format invalid!", stdout);     break;    }   }   break;  default:   putchar(c);   break;  }  format++; } va_end(ap);}int main(void){ myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B', "hello"); return 0;}