標籤:
通常我們使用的C函數的參數個數都是固定的,但也有不固定的。比如printf()與scanf()。如何自己動手實現一個可變參數函數,這個還是有點技巧的。
我們最常用的就是定義一個宏,使用printf或者printk,如下
#define wwlogk(fmt, args...) printk(fmt, ## args)
現在我們自己動手實現一個可變參數的函數,後面分析原理。首先看一個例子:
#include <stdio.h>
#include <stdarg.h>
int Sum(int first, int second, ...)//當無法列出傳遞函數的所有實
//參的類型和數目時,可用省略符號指定參數表
{
int sum = 0, t = first;
va_list vl;
va_start(vl, first);
while (t != -1){
sum += t;
t = va_arg(vl, int); //將當前參數轉換為int類型
}
va_end(vl);
return sum;
}
int main(int argc, char* argv[])
{
printf("The sum is %d\n", Sum(30, 20, 10, -1)); //-1是參數結束標誌
return 0;
}
在上面的例子中,實現了一個參數個數不定的求int型和的函數Sum()。
其中有幾個變數需要說明一下。va_list、va_start()、va_end和va_arg。
Va_list:該類型變數用來訪問可變參數,實際上就是指標。
Va_start():是一個宏,用來擷取參數列表中的參數,使vl指向第一個可變參數,使用完畢後調用va_end()結束。
va_end:也是一個宏,用來結束va_start()的調用。
va_arg:宏,用來獲參數列表中的取下一個值。
在linux原始碼中,include/acpi/platform/acenv.h,標頭檔有詳細描述。
1、 va_list vl;
typedef char *va_list; //定義了一個新的類型,指向字串的指標。其實真實意圖是當指標移動是以"1"單位,因為sizeof(char) =1;即char類型佔一個位元組,int型佔4個位元組。
2、 va_start(vl, first) 使vl指向第一個可變參數,即。
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
_bnd(X, bnd)的定義主要是為了某些需要記憶體的對齊的系統,這個宏的目的是為了得到最後一個固定參數的實際記憶體大小。直接用sizeof也沒有影響。
#define _AUPBND (sizeof (acpi_native_int) - 1)
其中acpi_native_int是根據硬體平台來決定的,即一個int類型的寬度,即位元組。
typedef s64 acpi_native_int;
typedef s32 acpi_native_int;
typedef int s32;
typedef long long s64;
下面就這段宏代碼解釋一下:
進程運行時,將變數壓入棧,而(char *) &(A)指向first,即棧頂;
使用宏_bnd (A,_AUPBND),主要是為了某些系統需要記憶體按照整數位元組對齊,因為C調用協議下面,參數入棧都是整數位元組(指標或者值)--- 所謂對齊,對Intel80x86 機器來說就是要求每個變數的地址都是sizeof(int)的倍數。那為什麼要對齊?因為在對齊下,CPU 的運行效率要快得多。
樣本:如,當一個long 型數(中long1)在記憶體中的位置正好與記憶體的字邊界對齊時,CPU 存取這個數只需訪問一次記憶體,而當一個long 型數(中的long2)在記憶體中的位置跨越了字邊界時,CPU 存取這個數就需要多次訪問記憶體,如i960cx 訪問這樣的數需讀記憶體三次(一個BYTE、一個SHORT、一個BYTE,由CPU 的微代碼執行,對軟體透明),所以對齊下CPU 的運行效率明顯快多了。
1 8 16 24 32
------- ------- ------- ---------
| long1 | long1 | long1 | long1 |
------- ------- ------- ---------
| | | | long2 |
------- ------- ------- ---------
| long2 | long2 | long2 | |
------- ------- ------- ---------
因為_AUPBND為int寬度,那麼_bnd(A, _AUPBND)的意思就是不夠一個int寬度的資料,將還是跳過一個int寬度。比如char、short類型的資料sizeof後為1和2,假設現在是32位系統char類型,_bnd(X, bnd) = (1 + 3 ) & (~3) = 0x4; 2也一樣。因為32位系統int寬度為4。跳過以後,ap指向second,而first的值已經儲存在t中。這裡需要說明的是,因為題設已經指定first為int型。若為其他型(如char)則會出現錯誤,因為這裡首先跳過了4個位元組。
圖1 棧的結構
另外這裡還需要注意幾點:
- 因為C語言壓棧順序為從右至左。
- 棧的擴充方向是向下擴充,所以棧底為高地址,棧頂為低地址。
比如假設f(a,b,c,d)按照從右至左壓棧,那麼d應該是第一個進棧的,a是最後一個進棧的,所以d的地址應該比a的高。在intel+ windows的機器上,函數棧的方向是向下的,棧頂指標的記憶體位址低於棧底指標,所以先進棧的資料是存放在記憶體的高地址處。
- C語言壓棧的時候,第一個進棧的是主函數中第一條指令的地址,然後依次是函數參數和局部變數。
如上所述,va_start(vl,first)後,vl指向first後面的第一個可變參數。我們都知道Pascal的參數入棧順序時自左向右的,但是C語言會是自右向左。為什麼呢?這也是C語言比pascal進階的一個地方--C語言通過這種參數入棧的順序實現了對變長參數函數的支援!
為了支援可變參數函數,C語言引入新的調用協議, 即C語言呼叫慣例 __cdecl . 採用C/C++語言編程的時候,預設使用這個呼叫慣例。如果要採用其它呼叫慣例,必須添加其它關鍵字聲明,例如WIN32 API使用PASCAL呼叫慣例,函數名字之前必須加__stdcall關鍵字。 採用C呼叫慣例時,函數的參數是從右至左入棧,個數可變。由於函數體不能預Crowdsourced Security Testing道傳進來的參數個數,因此採用本約定時必須由函數調用者負責堆棧清理。
3、#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
把這個宏展開可以看的更清楚:
- ap = ap + _bnd(T,_AUPBND) --首先將ap的跳過指定寬度,即指向下一個可變參數。
- *(T *)(ap - _bnd(T,_AUPBND)) --然後還原ap後,將其轉化為"T"型指標並求指標的值。
現在可以看清楚這個宏的意思是求當前ap指向的值,並將ap指向下一目標。
4、va_end(vl) 把vl指標清為NULL
#define va_end(ap) (void) 0
注意:這段代碼只有在windows下編譯和運行後,才能輸出正確的結果,在linux下,需要將int Sum(int first, int second, ...)修改為int Sum(int first, ...),即刪掉int second,因為second如果寫出來,就不是可變參數,first也不是可變參數。修改後的函數在linux下和windows下都可以正常運行。
搞清楚上面範例程式碼的原理後,我們可以自己手動實現這樣一個函數。
#include <stdio.h>
int Sum(int first, int second,...)
{
int sum = 0, t = first;
char * vl;//定義一個指標
vl = (char *)&first;//使指標指向第一個參數
while (*vl != -1)//-1是預先給定的結束符
{
sum += *(int *)vl;//類型轉換
vl += sizeof(int);//移動指標,使指標指向下一個參數
}
return sum;
}
int main(int argc, char* argv[])
{
printf("The sum is %d\n", Sum(30, 20, 10, -1));//-1是參數結束標誌
return 0;
}
實際上聲明一個可變參數有兩種方式:建議使用第一種。
第一種:包含標頭檔stdarg.h,採用ANSI標準形式,參數個數可變的函數的原型聲明是:
type funcname(type para1, type para2, ...)
第二種:包含標頭檔varargs.h,採用與UNIX System V相容的聲明方式時,參數個數可變的函數原型是:
type funcname(va_alist)
va_dcl
va_dcl為宏,宏定義原型後已經包含分號,所以使用時不用加分號。Va_dcl是對va_alist的詳細聲明。Va_dcl在代碼中必須原樣給 出,va_alist在VC中可以原樣給出,也可以略去,但在UNIX上的CC或Linux上的GCC中都要省略掉。
關於可變參數的傳遞問題
有人問到這個問題,假如我定義了一個可變參數函數,在這個函數內部又要調用其它可變參數函數,那麼如何傳遞參數呢?上面的例子都是使用宏va_arg逐個把參數提取出來使用,能否不提取,直接把它們傳遞給另外的函數呢?
我們先看printf的實現:
int __cdecl printf (const char *format, ...)
{
va_list arglist;
int buffing;
int retval;
va_start(arglist, format); //arglist指向format後面的第一個參數
...//不關心其它代碼
retval = _output(stdout,format,arglist); //把format格式和參數傳遞給output函數
...//不關心其它代碼
return(retval);
}
我們先模仿這個函數寫一個:
#include <stdio.h>
#include <stdarg.h>
int mywrite(char *fmt, ...)
{
va_list arglist;
va_start(arglist, fmt);
return printf(fmt,arglist);
}
void main()
{
int i=10, j=20;
char buf[] = "This is a test";
double f= 12.345;
mywrite("String: %s\nInt: %d, %d\nFloat :%4.2f\n", buf, i, j, f);
}
運行一下看看,錯誤百出。仔細分析原因,根據宏的定義我們知道 arglist是一個指標,它指向第一個可變的參數,但是所有的參數都位於棧中,所以arglist指向棧中某個位置,通過arglist的值,我們可以直接查看棧裡面的內容:
arglist -> 指向棧裡面,內容包括
0067FD78 E0 FD 67 00 //指向字串"This is a test"
0067FD7C 0A 00 00 00 //整數 i 的值
0067FD80 14 00 00 00 //整數 j 的值
0067FD84 71 3D 0A D7 //double 變數 f, 佔用8個位元組
0067FD88 A3 B0 28 40
0067FD8C 00 00 00 00
如果直接調用 printf(fmt, arglist); 僅僅是把arglist指標的值0067FD78入棧,然後把格式字串入棧,相當於調用:
printf(fmt, 0067FD78);
自然這樣的調用肯定會出現錯誤。
我們能不能逐個把參數提取出來,再傳遞給其它函數呢?先考慮一次性把所有參數傳遞進去的問題。
如果調用的是系統庫函數,這種情況下是不可能的。因為提取參數是在運行態,而參數入棧是在編譯的時候確定的。無法讓編譯器預知運行態的事情給出正確的參數入棧代碼。而我們在運行態雖然可以提取每個參數,但是無法將參數一次性全部壓棧,即使使用彙編代碼實現起來也是很困難的,因為不單是一個簡單的push代 碼就可以做到。
---------------------------------------------------------
問題一:
上面這段代碼經測試可以正常輸出。也就是說,我們通過使用指標,實現了參數不定的函數。但這裡還有一個問題,就是sum函數的所有參數都是int類型的,事先我們知道要移動sizeof(int)位的指標,可是如果參數類型不同呢?
答案與分析:這的確是個比較麻煩的問題,因為不同的資料類型佔用的位元組數可能是不一樣的(如double型為8個字元,short int型為2個),所以很難事先確定應該移動多少個位元組!但是辦法還是有的,這就是使用指標了,無論什麼類型的指標,都是佔用4個位元組,所以,可以把所有的傳如入參數都設定為指標,這樣一來,就可以通過移動固定的4個位元組來實現遍曆可變參數的目的了,至於如何取得指標中的內容並使用他們,當然也是無法預先得知的了。所以這大概也就是像printf(),scanf()之類的函數還需要一個格式控制符的原因吧^_^!不過實現起來還是有不少麻煩,暫且盜用vprintf()來實現一個與printf()函數一樣功能的函數了,代碼如下:
void myPrint(const char *frm, ...)
{
va_list vl;
va_start(vl, frm);
vprintf(frm, vl);
va_end(vl);
}
-----------------------------------------------------------
問題二: 還有一個問題,是上述問題的變體,不過意思相同:有沒有辦法寫一個函數,這個函數參數的具體形式可以在運行時才確定?
答案與分析:目前沒有"正規"的解決辦法,不過獨門偏方倒是有一個,因為有一個函數已經給我們做出了這方面的榜樣,那就是main(),它的原型是:
int main(int argc,char *argv[]);
函數的參數是argc和argv。
深入想一下,"只能在運行時確定參數形式",也就是說你沒辦法從聲明中看到所接受的參數,也即是參數根本就沒有固定的形式。常用的辦法是你可以通過定 義一個void *類型的參數,用它來指向實際的參數區,然後在函數中根據根據需要任意解釋它們的含義。這就是main函數中argv的含義,而argc,則用來表明實際的參數個數,這為我們使用提供了進一步的方便,當然,這個參數不是必需的。
雖然參數沒有固定形式,但我們必然要在函數中解析參數的意義,因此,理所當然會有一個要求,就是調用者和被調者之間要對參數區內容的格式,大小,有效性等所有方面達成一致,否則南轅北轍各說各話就慘了。
-------------------------------------------------------------
問題三:可變長參數的傳遞
有時候,需要編寫一個函數,將它的可變長參數直接傳遞給另外的函數,請問,這個要求能否實現?
答案與分析:目前,你尚無辦法直接做到這一點,但是我們可以迂迴前進,首先,我們定義被調用函數的參數為va_list類型,同時在調用函數中將可變長參數列錶轉換為va_list,這樣就可以進行變長參數的傳遞了。看如下所示:
void subfunc (char *fmt, va_list argp)
{
...
arg = va_arg (fmt, argp); /* 從argp中逐一取出所要的參數 */
...
}
void mainfunc (char *fmt, ...)
{
va_list argp;
va_start (argp, fmt); /* 將可變長參數轉換為va_list */
subfunc (fmt, argp); /* 將va_list傳遞給子函數 */
va_end (argp);
...
}
-------------------------------------------------------------
問題四:如何判別可變參數函數的參數類型?
函數形式如下:
void fun(char* str,...)
{
......
}
若傳的參數個數大於1,如何判別第2個以後傳參的參數類型???最好有源碼說明!
答案與分析:無法判斷。可變參數實現主要通過三個宏實現:va_start,va_arg,va_end。
如樓上所說,例如printf( "%d%c%s ", ....)是通過格式串中的%d,%c,%s來確定後面參數的類型,其實你也可以參考這種方法來判斷不定參數的類型。
-------------------------------------------------------------
問題五:定義可變長參數的一個限制
為什麼我的編譯器不允許我定義如下的函數,也就是可變長參數,但是沒有任何的固定參數?
int f (...)
{
...
}
答案與分析:不可以。這是ANSI C 所要求的,你至少得定義一個固定參數。
這個參數將被傳遞給va_start(),然後用va_arg()和va_end()來確定所有實際調用時可變長參數的類型和值。
---------------------------------------------------------------------
問題六:可變長參數的擷取
有這樣一個具有可變長參數的函數,其中有下列代碼用來擷取類型為float的實參:
va_arg (argp, float);
這樣做可以嗎?
答案與分析:不可以。在可變長參數中,應用的是"加寬"原則。也就是float類型被擴充成double;char, short被擴充成int。因此,如果你要去可變長參數列表中原來為float類型的參數,需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。
---------------------------------------------------------------------
問題七:可變長參數中類型為函數指標
我想使用va_arg來提取出可變長參數中類型為函數指標的參數,結果卻總是不正確,為什嗎?
答案與分析:這個與va_arg的實現有關。一個簡單的、示範版的va_arg實現如下:
#define va_arg(argp, type) (*(type *)(((argp) += sizeof(type)) - sizeof(type)))
其中,argp的類型是char *。
如果你想用va_arg從可變參數列表中提取出函數指標類型的參數,例如
int (*)(),則va_arg(argp, int (*)())被擴充為:
(*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))
顯然,(int (*)() *)是無意義的。
解決這個問題的辦法是將函數指標用typedef定義成一個獨立的資料類型,例如:
typedef int (*funcptr)();
這時候再調用va_arg(argp, funcptr)將被擴充為:
(* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))
這樣就可以通過編譯檢查了。
知識擴充
可能大家也猜到了,我擴充要擴充什麼了?!^_^
簡單介紹兩種函數呼叫慣例
__stdcall (C++預設)
- 參數從右向左壓入堆棧
- 函數被調用者修改堆棧
- 函數名(在編譯器這個層次)自動加前置的底線,後面緊跟一個@符號,其後緊跟著參數的尺寸
__cdecl (C語言預設)
- 參數從右向左壓入堆棧
- 參數由調用者清楚,手動清棧,被調用函數不會要求調用者傳遞多少參數,調用者傳遞過多或者過少的參數,甚至完全不同的參數都不會產生編譯階段的錯誤。
那麼,變參函數的調用方式為(也只能是):__cdecl 。
深度探索C語言函數可變長參數