可變參數參數在編程中其實是經常用得到的,查詢一些資料,整理了一下,最後寫出一個簡單的 宏定義的可變參數LOG,這個實例大家肯定可以經常用到。 可變參數定義 我們學習C語言時最經常使用printf()函數,但我們很少了解其原型。其實printf()的參數就是可變參數,想想看,我們可以利用它打印出各種類型的數據。下面我們來看看它的原型: intprintf( const char* format, ...); 它的第一個參數是format,屬於固定參數,後面跟的參數的個數和類型都是可變的(用三個點“…”做參數占位符),實際調用時可以有以下的形式: printf("%d",i); printf("%s",s); printf("thenumber is %d ,string is:%s", i, s); 說明例子 ①一個簡單的可變參數的C函數 在函數simple_va_fun參數列表中至少有一個整數參數,其後是占位符…表示後面參數的個數不定.。在這個例子裡,所有輸入參數必須都是整數,函數的功能只是打印所有參數的值。 #include<stdio.h> #include<stdarg.h> voidsimple_va_fun(int start, ...) { va_listarg_ptr; intnArgValue =start; intnArgCout=0; //可變參數的數目 va_start(arg_ptr,start);//以固定參數的地址為起點確定變參的內存起始地址。 do { ++nArgCout; printf("the%d th arg: %d\n",nArgCout,nArgValue); //輸出各參數的值 nArgValue= va_arg(arg_ptr,int); //得到下一個可變參數的值 }while(nArgValue != -1); return; } intmain(int argc, char* argv[]) { simple_va_fun(100,-1); simple_va_fun(100,200,-1); return0; } ②格式化到一個文件流,可用於日志文件 FILE*logfile; intWriteLog(const char * format, ...) { va_listarg_ptr; va_start(arg_ptr,format); intnWrittenBytes = vfprintf(logfile, format, arg_ptr); va_end(arg_ptr); returnnWrittenBytes; } 從這個函數的實現可以看到,我們使用可變參數應該有以下步驟: ⑴在程序中用到了以下這些宏: voidva_start( va_list arg_ptr, prev_param ); typeva_arg( va_list arg_ptr, type ); voidva_end( va_list arg_ptr ); va在這裡是variable-argument(可變參數)的意思. 這些宏定義在stdarg.h中,所以用到可變參數的程序應該包含這個頭文件. ⑵. 函數裡首先定義一個va_list型的變量,這裡是arg_ptr,這個變量是存儲參數地址的指針.因為得到參數的地址之後,再結合參數的類型,才能得到參數的值。 ⑶. 然後用va_start宏初始化⑵中定義的變量arg_ptr,這個宏的第二個參數是可變參數列表的前一個參數,即最後一個固定參數. ⑷. 然後依次用va_arg宏使arg_ptr返回可變參數的地址,得到這個地址之後,結合參數的類型,就可以得到參數的值。 ⑸. 設定結束條件 ① 是判斷參數值是否為-1。注意被調的函數在調用時是不知道可變參數的正確數目的,程序員必須自己在代碼中指明結束條件。 ② 是調用宏va_end。 剖析 va_*宏定義 我們已經知道va_start,va_arg,va_end是在stdarg.h中被定義成宏的, 由於硬件平台的不同和編譯器的不同,所以定義的宏也有所不同。 以下VC++6.0中stdarg.h裡的代碼 (文件的路徑為VC安裝目錄下的\vc98\include\stdarg.h) typedefchar * va_list; #define_INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #defineva_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #defineva_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #defineva_end(ap) ( ap = (va_list)0 ) linux中的定義 typedefchar *va_list; #define__va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) *sizeof (int)) #defineva_start(AP, LASTARG) (AP=((char*)&(LASTARG) + __va_rounded_size (LASTARG)) voidva_end (va_list); #defineva_end(AP) (AP=(char *)0) #defineva_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),\ *((TYPE*)(AP - __va_rounded_size (TYPE)))) 要理解上面這些宏定義的意思,需要首先了解: 1. 棧的方向和參數的入棧順序 2. CPU的對齊方式 3. 內存地址的表達方式。 1. 棧 以Intel 32位的CPU為分析基礎 在Intel CPU中,棧的生長方向是向下的,即棧底在高地址,而棧頂在低地址;從棧底向棧頂看過去,地址是從高地址走向低地址的,因為稱它為向下生長,如圖。 從上面壓棧前後的兩個圖可明顯看到棧的生長方向,在Intel32位的CPU中,windown或linux都使用了它的保護模式,ss指定棧所有在的段,ebp指向棧基址,esp指向棧頂。顯然執行push指令後,esp的值會減4,而pop後,esp值增加4。 棧中每個元素存放空間的大小決定push或pop指令後esp值增減和幅度。Intel32位CPU中的棧元素大小為16位或32位,由定義堆棧段時定義。在Window和Linux系統中,內核代碼已定義好棧元素的大小為32位,即一個字長(sizeof(int))。因此用戶空間程棧元素的大小肯定為32位,這樣每個棧元素的地址向4字節對齊。 C語言的函數調用約定對編寫可變參數函數是非常重要的,只有清楚了,才更欲心所欲地控制程序。在高級程序設計語言中,函數調用約定有如下幾種,stdcall,cdecl,fastcall ,thiscal,naked call。cdel是C語言中的標准調用約定,如果在定義函數中不指明調用約定(在函數名前加上約定名稱即可),那編譯器認為是cdel約定,從上面的幾種約定來看,只有cdel約定才可以定義可變參數函數。下面是cdel約定的重要特征:如果函數A調用函數B,那麼稱函數A為調用者(caller),函數B稱為被調用者(callee)。caller把向callee傳遞的參數存放在棧中,並且壓棧順序按參數列表中從右向左的順序;callee不負責清理棧,而是由caller清理。 我們用一個簡單的例子來說明問題,並采用Nasm的匯編格式寫相應的匯編代碼,程序段如下: voidcallee(int a, int b) { int c= 0; c = a+b; } voidcaller() { callee(1,2); } 來分析一下在調用過程發生了什麼事情。程序執行點來到caller時,那將要執行調用callee函數,在跳到callee函數前,它先要把傳遞的參數壓到棧上,並按右到左的順序,即翻譯成匯編指令就是push 2; push 1; 圖2 函數棧如圖中(a)所示。接著跳到callee函數,即指令callcalle。CPU在執行call時,先把當前的EIP寄存器的值壓到棧中,然後把EIP值設為callee(地址),這樣,棧的圖變為如圖2(b)。程序執行點跳到了callee函數的第一條指令。C語言在函數調用時,每個函數占用的棧段稱為stackframe。用ebp來記住函數stackframe的起始地址。故在執行callee時,最前的兩條指令為: pushebp movesp, ebp 經過這兩條語句後,callee函數的stackframe就建好了,棧的最新情況如圖2(c)所示。 函數callee定義了一個局部變量int c,該變量的儲存空間分配在callee函數占用的棧中,大小為4字節(insizeof int)。那麼callee會在如下指令: subesp, 4 mov[ebp-4], 0 這樣棧的情況又發生了變化,最新情況如圖2(d)所示。注意esp總是指向棧頂,而ebp作為函數的stackframe基址起到很大的作用。ebp地址向下的空間用於存放局部變量,而它向上的空間存放的是caller傳遞過來的參數,當然編譯器會記住變量c相對ebp的地址偏移量,在這裡為-4。跟著執行c = a+ b語句,那麼指令代碼應該類似於: moveax , [ebp + 8] ;這裡用eax存放第一個傳遞進來的參數,記住第一個參數與ebp的偏移量肯定為8addeax, [ebp + 12] ;第二個參數與ebp的偏移量為12,故計算eax =a+b mov[ebp -4], eax ;執行 c =eax, 即c =a+b 棧又有了新了變化,如圖2(e)。至此,函數callee的計算指令執行完畢,但還要做一些事情:釋放局部變量占用的棧空間,銷除函數的stack-frame過程會生成如下指令:movesp, ebp;把局部變量占用的空間全部略過,即不再使用,ebp以下的空間全部用於局部變量pop ebp;彈出caller函數的stack-frame 基址在Intel CPU裡上面兩條指令可以用指令leave來代替,功能是一樣。這樣棧的內容如圖2(f)所示。最後,要返回到caller函數,因此callee的最後一條指令是ret,ret指令用於把棧上的保存的斷點彈出到EIP寄存器,新的棧內容如圖2(g)所示。函數callee的調用與返回全部結束,跟著下來是執行callcallee的下一條語句。 從caller函數調用callee前,把傳遞的參數壓到棧中,並且按從右到左的順序;函數返回時,callee並不清理棧,而是由caller清楚傳遞參數所占用的棧(如上圖,函數返回時,1和2還放在棧中,讓caller清理)。棧元素的大小為4個字節,每個參數占用棧空間大小為4字節的倍數,並且任何兩個參數都不能共用同一個棧元素。 從C語言的函數調用約定可知,參數列表從右向左依次壓棧,故可變參數壓在棧的地址比最後一個命名參數還大,如下圖3所示: 由圖3可知,最後一個命名參數a上面都放著可變參數,每個參數占用棧的大小必為4的倍數。因此:可變參數1的地址 = 參數a的地址 + a占用棧的大小,可變參數2的地址 = 可變參數1的地址 + 可變參數1占用棧的大小,可變參數3的地址 = 可變參數2的地址 + 可變參數2占用棧的大小,依此類推。如何計算每個參數占用棧的大小呢? 2. 數據對齊問題 對於兩個正整數 x, n 總存在整數 q, r 使得 x = nq+ r, 其中0<= r <n //最小非負剩余 q, r 是唯一確定的。q =[x/n], r = x - n[x/n]. 這個是帶余除法的一個簡單形式。在 c 語言中, q, r 容易計算出來: q =x/n, r = x % n. 所謂把 x 按 n 對齊指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 這也相當於把 x 表示為: x = nq+ r', 其中 -n< r' <=0 //最大非正剩余 nq 是我們所求。關鍵是如何用 c 語言計算它。由於我們能處理標准的帶余除法,所以可以把這個式子轉換成一個標准的帶余除法,然後加以處理: x+n =qn + (n+r'),其中0<n+r'<=n //最大正剩余 x+n-1= qn + (n+r'-1), 其中0<= n+r'-1 <n //最小非負剩余 所以 qn =[(x+n-1)/n]n. 用 c 語言計算就是: ((x+n-1)/n)*n 若 n 是 2 的方冪, 比如 2^m,則除為右移 m 位,乘為左移 m 位。所以把 x+n-1的最低 m 個二進制位清 0就可以了。得到: (x+n-1)& (~(n-1)) 根據這些推導,相信已經了解#define__va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) *sizeof (int))的涵義。 3. 再看va_* 宏定義 va_start(va_listap, last) last為最後一個命名參數,va_start宏使ap記錄下第一個可變參數的地址,原理與“可變參數1的地址 = 參數a的地址 + a占用棧的大小”相同。從ap記錄的內存地址開始,認為參數的數據類型為type並把它的值讀出來;把ap記錄的地址指向下一個參數,即ap記錄的地址 +=occupy_stack(type) va_arg(va_litap, type) 這裡是獲得可變參數的值,具體工作是:從ap所指向的棧內存中讀取類型為type的參數,並讓ap根據type的大小記錄它的下一個可變參數地址,便於再次使用va_arg宏。從ap記錄的內存地址開始,認為存的數據類型為type並把它的值讀出來;把ap記錄的地址指向下一個參數,即ap記錄的地址 +=occupy_stack(type) va_end(va_listap) 用於“釋放”ap變量,它與va_start對稱使用。在同一個函數內有va_start必須有va_end。 可變參數宏定義 printf()和fprintf()這些輸出函數的參數是可變的,在調試程序時,你可能希望定義自己的參數可變的輸出函數, 那麼可變參數宏會是一個選擇。 C99中規定宏可以像函數一樣帶有可變參數,比如 #define LOG(format, ...)fprintf(stdout, format, __VA_ARGS__) 其中,...表示參數可變,__VA_ARGS__在預處理中為實際的參數集所替換 GCC中同時支持如下的形式 #define LOG(format, args...) fprintf(stdout,format, args) 其用法和上面的基本一致,只是參數符號有變化 有一點需要注意,上述的宏定義不能省略可變參數,盡管你可以傳遞一個空參數,這裡有必要提到"##"連接符號的用法。 "##"的作用是對token進行連接,在上例中,format、__VA_ARGS__、args即是token, 如果token為空,那麼不進行連接,所以允許省略可變參數(__VA_ARGS__和args),對上述變參宏做如下修改 #define LOG(format, ...) fprintf(stdout, format, ##__VA_ARGS__) #define LOG(format, args...) fprintf(stdout,format, ##args) 上述的變參宏定義不僅能自定義輸出格式,而且配合#ifdef #else#endif在輸出管理上也很方便, 比如調試時輸出調試信息,正式發布時則不輸出,可以這樣 #ifdef DEBUG #define LOG(format, ...) fprintf(stdout,">> "format"\n", ##__VA_ARGS__) #else #define LOG(format, ...) #endif 在調試環境下,LOG宏是一個變參輸出宏,以自定義的格式輸出; 在發布環境下,LOG宏是一個空宏,不做任何事情。 寫日志實例 #ifndef _HYC_LOG_H_ #define _HYC_LOG_H_ #include "base.h" #include <stdarg.h> #include <string> #include <iostream> // #ifdef LOG_COUT #define LOG_TRACE(strMsg,...) \ {\ char ch[1024] ;\ sprintf(ch,"%s %d %s %s",__FILE__,__LINE__,__DATE__,__TIME__);\ std::cout << ch << " " << minprintf(strMsg,##__VA_ARGS__)<< std::endl ;\ } #else // #define LOG_TRACE(strMsg,...) \ {\ char ch[1024] ;\ sprintf(ch,"%s %d %s %s",__FILE__,__LINE__,__DATE__,__TIME__);\ char fileName[] = "huangxw.log" ; \ std::ofstream outFile(fileName,std::ios::out|std::ios::app); \ outFile << ch << " " <<minprintf(strMsg,##__VA_ARGS__) << std::endl; \ outFile.close(); \ } #endif // // std::string minprintf(char *fmt, ...) { va_list ap; /* points to each unnamed arg in turn */ char *p, *sval; char tVal[128] ; int ival; double dval; std::string strTotal ; va_start(ap, fmt); /* make ap point to 1st unnamed arg */ for (p = fmt; *p; p++) { if (*p != '%') { //putchar(*p); strTotal += *p ; continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); sprintf(tVal,"%d",ival); strTotal += tVal; break; case 'x': ival=va_arg(ap,int); sprintf(tVal,"%#x",ival); strTotal += tVal; break; case 'f': dval = va_arg(ap, double); sprintf(tVal,"%f",dval); strTotal += tVal; break; case 's': for (sval = va_arg(ap, char *);*sval; sval++) strTotal += *sval ; break; default: strTotal += *p ; break; } }www.2cto.com va_end(ap); /* clean up when done */ return strTotal; } #endif test程序: #include "../hyc_log.h" int main() { LOG_TRACE("test:%d",10); LOG_TRACE("test:%s=%d","max",10); LOG_TRACE("test"); }