如何使用變參函式?
對於C語言的初學者來說printf函式是經常被使用的。但是不知道大家在初學的時候有沒有注意到printf這個函式可以定義成這樣:
printf("%d",a); //有兩個形參
也可以是這樣:
printf("%d %d",a,b); //有三個形參
可以發現printf函式可以定義任意多個形參。所以printf函式是一個變參函式。
那麼,變參函式的原理究竟是什麼呢,我們如何去書寫變參函式?
在這裡我寫了一個簡單的變參函式和大家分享一下,程式碼如下:
#include <stdio.h>
#define _AUPBND (sizeof (int) - 1)
#define _ADNBND (sizeof (int) - 1)
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define MYva_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define MYva_end(ap) (void) 0
#define MYva_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
int sum(int n,...) //利用變參函式求和
{
int buf[20]; //快取陣列,用於儲存傳入的形參
int sum = 0,i;
char* t; //定義一個char*型別的變數
MYva_start(t, n); //將第二個形參的地址賦給t變數
for(i = 0;i < n;i++) //迴圈進行取後邊的形參和進行求和計算
{
buf[i] = MYva_arg(t,int); //取後面的形參並將當前形參的值取出賦給buf快取陣列
sum += buf[i]; //求和運算
}
MYva_end(t); //結束,將t指向NULL (雖然註釋掉對結果沒有影響,但是為了程式邏輯的嚴謹,還是加上吧。)
return sum; //返回計算結果
}
main()
{ //sum函式第一個引數是需要求和的數的個數,後邊的是要求和的數值
printf("%d",sum(4,1,2,3,6));
}
先簡單做個說明。C編譯器通常提供了一系列處理可變引數的巨集,以遮蔽不同的硬體平臺造成的差異,增加程式的可移植性。這些巨集包括va_start、va_arg和va_end等。但是我們只是寫一個簡單的變參函式,巨集的名字等當然可以自己隨便取啦。
我猜初學者看到程式碼開頭定義的巨集函式,一定會蒙圈的。那麼我們就來簡單分析一下。
眾所周知,函式的形參是通過棧來傳遞的。所以說,變參函式至少有一個引數是確定的。假定我們的棧是向低地址生長的(x86架構的計算機棧是向低地址生長),而函式入棧的方向是從右向左入棧。也就是說,最後一個形參最先入棧。所以,呼叫函式sum(4,1,2,3,6)之後的棧是這樣的:
(低地址在上) | 棧: |
---|---|
第一個形參: | 4 |
第二個形參: | 1 |
… | … |
最後一個形參: | 6 |
因為現在函式只知道第一個形參的資料型別和值,所以就需要順藤摸瓜,通過那幾個巨集函式在棧中找到後面的n個形參。
知道了這些,變參函式的原理就基本掌握了,那幾個巨集函式理解起來也不是那麼費勁了。
我們以MYva_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))這個為例:
此巨集函式的作用是將ap指標指向第一個不確定的形參,以我們的函式sum(4,1,2,3,6)為例,就是由第一個形參,根據棧的原理,找到第二個形參的地址。所以呼叫此巨集函式之後,ap的地址就指向了引數’1’的地址。我們的程式在執行過程中,定義了char* t,將t作為實參傳入了這個巨集函式中,所以現在t就指向了引數’1’。
簡單點講,((char *) &(A))就是獲取了第一個引數的地址,(_bnd (A,_AUPBND))計算出了A所佔的地址空間,兩者相加就得到了下一個引數的地址。所以我們將第一個形參n作為實參傳入此巨集函式進行計算。
到了這裡大家可能又會有一個疑問。計算A佔的地址空間,直接(sizeof (A))不就完了嗎?為什麼還要(_bnd (A,_AUPBND))呢?
我們找到了_bnd這個巨集函式的定義:
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
這樣做其實是對sizeof後的結果進行的向上取了一個最接近bnd+1的倍數的數。bnd又是什麼呢,我們找到bnd對應的實參_AUPBND,進而找到了_AUPBND的巨集定義:
#define _AUPBND (sizeof (int) - 1)
所以我們知道巨集函式_bnd其實是對sizeof後的結果向上取了一個sizeof (int)的倍數的數。假設你的cpu執行sizeof (int)的結果為4,再假設你在巨集函式_bnd傳入的X經過sizeof (X)計算的結果為17,那麼巨集函式_bnd的結果為就20。為什麼會這樣呢?因為棧的最小單元就是sizeof (int)。
好了,介紹完MYva_start(ap, A)這個巨集函式的原理,相信其他巨集函式也可以理解了。進而就可以掌握變參函式的原理。
但是變參函式由於語法並不是非常嚴謹,比如當你呼叫printf(“%d”,a,b);的時候,b就被忽略了,可能得不到你想要的結果,在應用中,變參函式也大多能被代替,所以變參函式的實際應用價值可能沒那麼大。
本人也是C語言的初學者,菜鳥一枚,如果出現錯誤或者不合適的地方,請斧正,謝謝。