C語言可變參數

轉自http://www.cnblogs.com/wangyonghui/archive/2010/07/12/1776068.html,稍有改動

一、是什麼

我們學習C語言時最經常使用printf()函數,但我們很少了解其原型。其實printf()的參數就是可變參數,想想看,我們可以利用它打印出各種類型的數據。下面我們來看看它的原型:

int printf( const char* format, ...);

它的第一個參數是format,屬於固定參數,後面跟的參數的個數和類型都是可變的(用三個點“…”做參數佔位符),實際調用時可以有以下的形式:

printf("%d",i); 
printf("%s",s); 
printf("the number is %d ,string is:%s", i, s); 

那麼它的原型是怎樣實現的呢?我今天在看內核代碼時碰到了vsprintf,花了大半天時間,終於把它搞的有點明白了。

二、先看兩個例子

不必弄懂,先大致瞭解其用法,繼續往下看。

①一個簡單的可變參數的C函數

在函數simple_va_fun參數列表中至少有一個整數參數,其後是佔位符…表示後面參數的個數不定.。在這個例子裏,所有輸入參數必須都是整數,函數的功能只是打印所有參數的值。

#include <stdio.h>
#include <stdarg.h>
void simple_va_fun(int start, ...) 
{ 
       va_list arg_ptr; 
       int nArgValue =start;
       int nArgCout=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; 
}
int main(int argc, char* argv[])
{
       simple_va_fun(100,-1); 
       simple_va_fun(100,200,-1); 
       return 0;
}

②格式化到一個文件流,可用於日誌文件

FILE *logfile;
int WriteLog(const char * format, ...)
{
va_list arg_ptr;
va_start(arg_ptr, format);
int nWrittenBytes = vfprintf(logfile, format, arg_ptr);
va_end(arg_ptr);
return nWrittenBytes;
}

稍作解釋上面兩個例子。

【這部分的引用地址http://www.cppblog.com/lmlf001/archive/2006/04/19/5874.html

從這個函數的實現可以看到,我們使用可變參數應該有以下步驟:

⑴在程序中用到了以下這些宏:

void va_start( va_list arg_ptr, prev_param ); 
type va_arg( va_list arg_ptr, type ); 
void va_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

三、剖析可變參數真相

1.va_*宏定義

我們已經知道va_start,va_arg,va_end是在stdarg.h中被定義成宏的,由於1)硬件平臺的

不同 2)編譯器的不同,所以定義的宏也有所不同。下面看一下VC++6.0stdarg.h裏的代碼

(文件的路徑爲VC安裝目錄下的\vc98\include\stdarg.h

typedef char *  va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

再來看看linux中的定義

typedef char *va_list;
#define __va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
#define va_start(AP, LASTARG) (AP=((char*)&(LASTARG) + __va_rounded_size (LASTARG))
void va_end (va_list);
#define va_end(AP) (AP= (char *)0)
#define va_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),\
*((TYPE *)(AP - __va_rounded_size (TYPE))))

要理解上面這些宏定義的意思,需要首先了解:

①棧的方向②參數的入棧順序③CPU的對齊方式④內存地址的表達方式。

2.棧——以Intel32位的CPU爲分析基礎

IntelCPU中,棧的生長方向是向下的,即棧底在高地址,而棧頂在低地址;從棧底向棧頂看過去,地址是從高地址走向低地址的,因爲稱它爲向下生長,如圖。

 

【圖1引用自http://www.yuanma.org/data/2008/0504/article_3027_1.htm,這部分內容,我認爲作者講的很詳細,所以引來共享】

從上面壓棧前後的兩個圖可明顯看到棧的生長方向,在Intel32位的CPU中,windownlinux都使用了它的保護模式,ss指定棧所有在的段,ebp指向棧基址,esp指向棧頂。顯然執行push指令後,esp的值會減4,而pop後,esp值增加4。棧中每個元素存放空間的大小決定pushpop指令後esp值增減和幅度。Intel32CPU中的棧元素大小爲16位或32位,由定義堆棧段時定義。在WindowLinux系統中,內核代碼已定義好棧元素的大小爲32位,即一個字長(sizeof(int))。因此用戶空間程棧元素的大小肯定爲32位,這樣每個棧元素的地址向4字節對齊。

C語言的函數調用約定對編寫可變參數函數是非常重要的,只有清楚了,才更欲心所欲地控制程序。在高級程序設計語言中,函數調用約定有如下幾種,stdcallcdeclfastcall,thiscal,nakedcallcdelC語言中的標準調用約定,如果在定義函數中不指明調用約定(在函數名前加上約定名稱即可),那編譯器認爲是cdel約定,從上面的幾種約定來看,只有cdel約定纔可以定義可變參數函數。下面是cdel約定的重要特徵:如果函數A調用函數B,那麼稱函數A爲調用者(caller),函數B稱爲被調用者(callee)caller把向callee傳遞的參數存放在棧中,並且壓棧順序按參數列表中從右向左的順序;callee不負責清理棧,而是由caller清理。我們用一個簡單的例子來說明問題,並採用Nasm的彙編格式寫相應的彙編代碼,程序段如下:

void callee(int a, int b)
{
int c = 0;
c = a +b;
} 
void caller()
{
callee(1,2);
} 

來分析一下在調用過程發生了什麼事情。程序執行點來到caller時,那將要執行調用callee函數,在跳到callee函數前,它先要把傳遞的參數壓到棧上,並按右到左的順序,即翻譯成彙編指令就是push2push1

2

函數棧如圖中(a)所示。接着跳到callee函數,即指令callcalleCPU在執行call時,先把當前的EIP寄存器的值壓到棧中,然後把EIP值設爲callee(地址),這樣,棧的圖變爲如圖2(b)。程序執行點跳到了callee函數的第一條指令。C語言在函數調用時,每個函數佔用的棧段稱爲stackframe。用ebp來記住函數stackframe的起始地址。故在執行callee時,最前的兩條指令爲:

push ebp
mov ebp, esp

經過這兩條語句後,callee函數的stackframe就建好了,棧的最新情況如圖2(c)所示。函數callee定義了一個局部變量intc,該變量的儲存空間分配在callee函數佔用的棧中,大小爲4字節(insizeofint)。那麼callee會在如下指令:

sub esp, 4
mov [ebp-4], 0

這樣棧的情況又發生了變化,最新情況如圖2(d)所示。注意esp總是指向棧頂,而ebp作爲函數的stackframe基址起到很大的作用。ebp地址向下的空間用於存放局部變量,而它向上的空間存放的是caller傳遞過來的參數,當然編譯器會記住變量c相對ebp的地址偏移量,在這裏爲-4。跟着執行c= a + b語句,那麼指令代碼應該類似於:

mov eax , [ebp +  8] ;這裏用eax存放第一個傳遞進來的參數,記住第一個參數與ebp的偏移量肯定爲8
add eax,  [ebp + 12] ;第二個參數與ebp的偏移量爲12,故計算eax = a+b
mov [ebp -4], eax  ;執行 c = eax, 即c = a+b

棧又有了新了變化,如圖2(e)。至此,函數callee的計算指令執行完畢,但還要做一些事情:釋放局部變量佔用的棧空間,銷除函數的stack-frame過程會生成如下指令:

movesp, ebp;把局部變量佔用的空間全部略過,即不再使用,ebp以下的空間全部用於局部變量

popebp;彈出caller函數的stack-frame基址

IntelCPU裏上面兩條指令可以用指令leave來代替,功能是一樣。這樣棧的內容如圖2(f)所示。最後,要返回到caller函數,因此callee的最後一條指令是

ret

ret指令用於把棧上的保存的斷點彈出到EIP寄存器,新的棧內容如圖2(g)所示。函數callee的調用與返回全部結束,跟着下來是執行callcallee的下一條語句。

caller函數調用callee前,把傳遞的參數壓到棧中,並且按從右到左的順序;函數返回時,callee並不清理棧,而是由caller清楚傳遞參數所佔用的棧(如上圖,函數返回時,12還放在棧中,讓caller清理)。棧元素的大小爲4個字節,每個參數佔用棧空間大小爲4字節的倍數,並且任何兩個參數都不能共用同一個棧元素。

下面是使用gcc  -S 生成的AT&T格式彙編代碼

	.file	"test.c"
	.text
	.globl	callee
	.type	callee, @function
callee:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	movl	$0, -4(%ebp)
	movl	12(%ebp), %eax
	movl	8(%ebp), %edx
	addl	%edx, %eax
	movl	%eax, -4(%ebp)
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	callee, .-callee
	.globl	caller
	.type	caller, @function
caller:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$8, %esp
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	callee
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	caller, .-caller
	.ident	"GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
	.section	.note.GNU-stack,"",@progbits

其中callee中
subl	$16, %esp
可見AT&T是預留一段空間棧

C語言的函數調用約定可知,參數列表從右向左依次壓棧,故可變參數壓在棧的地址比最後一個命名參數還大,如下圖3所示:

由圖3可知,最後一個命名參數a上面都放着可變參數,每個參數佔用棧的大小必爲4的倍數。因此:可變參數1的地址=參數a的地址+a佔用棧的大小,可變參數2的地址=可變參數1的地址+可變參數1佔用棧的大小,可變參數3的地址=可變參數2的地址+可變參數2佔用棧的大小,依此類推。如何計算每個參數佔用棧的大小呢?

3.數據對齊問題

對於兩個正整數 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.

 

所謂把 xn對齊指的是:若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

n2的方冪,比如2^m,則除爲右移m位,乘爲左移m位。所以把x+n-1的最低m個二進制位清0就可以了。得到:

(x+n-1)& (~(n-1))

【來自CSDN博客:http://blog.csdn.net/swell624/archive/2008/11/03/3210779.aspx

根據這些推導,相信已經瞭解#define__va_rounded_size(TYPE)  (((sizeof (TYPE) + sizeof (int) - 1) /sizeof (int)) * sizeof (int))的涵義。

       4.再看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

5.可變參數函數問題

考慮了參數大小和數據對齊問題,使得可變參數的類型不但可以是基本類型,同樣適用於用戶定義類型。值的注意的是,如果是用戶定義類型,最好用typedef定義的名字作爲類型名,這樣就會減少在va_arg進行宏展開時出錯的機率。

在可變參數函數中,由va_list變量來記錄(或獲得)可變參數部分,但是va_list中並沒有記錄下它們的名字,事實上也是不可能的。要想把可變參數部分傳遞給下一個函數,唯有通過va_list變量去傳遞,而原來定義的函數用"..."來表示可變參數部分,而不是用va_list來表示。爲了方便程序的標準化,ANSIC在標準庫代碼中就作出了很好的榜樣:在任何形如:type fun( type arg1, type arg2,...)的函數,都同時定義一個與它功能完全一樣的函數,但用va_list類型來替換"...",即

typefun(type arg1, type arg2, va_list ap)。以printf函數爲例:
intprintf(const char *format, ...);    
intvprintf(const char *format, va_list ap);


第一個函數用"..."表示可變參數,第二個用va_list類型表示可變參數,目的是用於被其它可變參數調用,兩者在功能功能上是完全上一樣。只是在函數名字相差一個'"v"字母。

四、可變參數函數的應用

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
/* minprintf: minimal printf with variable argument list */
void minprintf(char *fmt, ...)
{
       va_list ap; /* points to each unnamed arg in turn */
       char *p, *sval;
       int ival;
       double dval;
       va_start(ap, fmt); /* make ap point to 1st unnamed arg */
       for (p = fmt; *p; p++) {
              if (*p != '%') {
                     putchar(*p);
                     continue;
              }
              switch (*++p) {
                 case 'd':
                        ival = va_arg(ap, int);
                        printf("%d", ival);
                        break;
                 case 'x':
                        ival=va_arg(ap,int);
                        printf("%#x",ival);
                        break;
                 case 'f':
                        dval = va_arg(ap, double);
                        printf("%f", dval);
                        break;
                 case 's':
                        for (sval = va_arg(ap, char *); *sval; sval++)
                               putchar(*sval);
                        break;
                 default:
                        putchar(*p);
                        break;
              }
       }
       va_end(ap); /* clean up when done */
}
 
int main(int argc, char* argv[])
{
       int i = 1234;
       int j = 5678;
       char *s="nihao";
       double f=;
 
       minprintf("the first test:i=%d\n",i,j); 
       minprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j); 
       minprintf("the 3rd test:s=%s\n",s); 
       minprintf("the 4th test:f=%f\n",f); 
       minprintf("the 5th test:s=%s,f=%f\n",s,f); 
       system("pause");
       return 0;
}
 
//不使用va_*宏定義的實現:
void minprintf(char* fmt, ...) //一個簡單的類似於printf的實現不過參數必須都是int 類型
{ 
       char* pArg=NULL;               //等價於原來的va_list 
       char c;
       pArg = (char*) &fmt; //注意不要寫成p = fmt !因爲這裏要對//參數取址,而不是取值
       pArg += sizeof(fmt);         //等價於原來的va_start        
       do
       {
              c =*fmt;
              if (c != '%')
              {
                     putchar(c);            //照原樣輸出字符
              }
              else
              {
                     //按格式字符輸出數據
                     switch(*++fmt) 
                     {
                     case 'd':
                            printf("%d",*((int*)pArg));           
                            break;
                     case 'x':
                            printf("%#x",*((int*)pArg));
                            break;
                     default:
                            break;
                     } 
                     pArg += sizeof(int);               //等價於原來的va_arg
              }
              ++fmt;
       }while (*fmt != '\0'); 
       pArg = NULL;                               //等價於va_end
       return; 
}




發佈了111 篇原創文章 · 獲贊 86 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章