詳解 C語(yǔ)言可變參數(shù) va_第1頁(yè)
詳解 C語(yǔ)言可變參數(shù) va_第2頁(yè)
詳解 C語(yǔ)言可變參數(shù) va_第3頁(yè)
詳解 C語(yǔ)言可變參數(shù) va_第4頁(yè)
詳解 C語(yǔ)言可變參數(shù) va_第5頁(yè)
已閱讀5頁(yè),還剩1頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說(shuō)明:本文檔由用戶(hù)提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

C語(yǔ)言的變長(zhǎng)參數(shù)在平時(shí)做開(kāi)發(fā)時(shí)很少會(huì)在自己設(shè)計(jì)的接口中用到,但我們最常用的接口printf就是使用的變長(zhǎng)參數(shù)接口,在感受到printf強(qiáng)大的魅力的同時(shí),是否想挖據(jù)一下到底printf是如何實(shí)現(xiàn)的呢?這里我們一起來(lái)挖掘一下C語(yǔ)言變長(zhǎng)參數(shù)的奧秘。先考慮這樣一個(gè)問(wèn)題:如果我們不使用C標(biāo)準(zhǔn)庫(kù)(libc)中提供的Facilities,我們自己是否可以實(shí)現(xiàn)擁有變長(zhǎng)參數(shù)的函數(shù)呢?我們不妨試試。一步一步進(jìn)入正題,我們先看看固定參數(shù)列表函數(shù),voidfixed_args_func(inta,doubleb,char*c){printf("a=0x%p\n",&a);printf("b=0x%p\n",&b);printf("c=0x%p\n”,&c);}對(duì)于固定參數(shù)列表的函數(shù),每個(gè)參數(shù)的名稱(chēng)、類(lèi)型都是直接可見(jiàn)的,他們的地址也都是可以直接得到的,比如:通過(guò)&a我們可以得到a的地址,并通過(guò)函數(shù)原型聲明了解到a是int類(lèi)型的;通過(guò)&b我們可以得到b的地址,并通過(guò)函數(shù)原型聲明了解到b是double類(lèi)型的;通過(guò)&c我們可以得到c的地址,并通過(guò)函數(shù)原型聲明了解到c是char*類(lèi)型的。但是對(duì)于變長(zhǎng)參數(shù)的函數(shù),我們就沒(méi)有這么順利了。還好,按照C標(biāo)準(zhǔn)的說(shuō)明,支持變長(zhǎng)參數(shù)的函數(shù)在原型聲明中,必須有至少一個(gè)最左固定參數(shù)(這一點(diǎn)與傳統(tǒng)C有區(qū)別,傳統(tǒng)C允許不帶任何固定參數(shù)的純變長(zhǎng)參數(shù)函數(shù)),這樣我們可以得到其中固定參數(shù)的地址,但是依然無(wú)法從聲明中得到其他變長(zhǎng)參數(shù)的地址,比如:voidvar_args_func(constchar*fmt,...){......}這里我們只能得到fmt這固定參數(shù)的地址,僅從函數(shù)原型我們是無(wú)法確定”...”中有幾個(gè)參數(shù)、參數(shù)都是什么類(lèi)型的,自然也就無(wú)法確定其位置了。那么如何可以做到呢?在大腦中回想一下函數(shù)傳參的過(guò)程,無(wú)論"..."中有多少個(gè)參數(shù)、每個(gè)參數(shù)是什么類(lèi)型的,它們都和固定參數(shù)的傳參過(guò)程是一樣的,簡(jiǎn)單來(lái)講都是棧操作,而棧這個(gè)東西對(duì)我們是開(kāi)放的。這樣一來(lái),一旦我們知道某函數(shù)幀的棧上的一個(gè)固定參數(shù)的位置,我們完全有可能推導(dǎo)出其他變長(zhǎng)參數(shù)的位置,順著這個(gè)思路,我們繼續(xù)往下走,通過(guò)一個(gè)例子來(lái)詮釋一下:(這里要說(shuō)明的是:函數(shù)參數(shù)進(jìn)棧以及參數(shù)空間地址分配都是”實(shí)現(xiàn)相關(guān)”的,不同平臺(tái)、不同編譯器都可能不同,所以下面的例子僅在IA-32,WindowsXP,MinGWgccv3.4.2下成立)我們先用上面的那個(gè)fixed_args_func函數(shù)確定一下這個(gè)平臺(tái)下的入棧順序。intmain(){fixed_args_func(17,5.40,"helloworld");return0;}a=0x0022FF50b=0x0022FF54c=0X0022FF5C從這個(gè)結(jié)果來(lái)看,顯然參數(shù)是從右到左,逐一壓入棧中的(棧的延伸方向是從高地址到低地址,棧底的占領(lǐng)著最高內(nèi)存地址,先入棧的參數(shù),其地理位置也就最高了)。我們基本可以得出這樣一個(gè)結(jié)論:c.addr=b.addr+x_sizeof(b);/*注意:x_sizeof!=sizeof,后話(huà)再說(shuō)*/b.addr=a.addr+x_sizeof(a);有了以上的”等式”,我們似乎可以推導(dǎo)出voidvar_args_func(constchar*fmt,...)函數(shù)中,可變參數(shù)的位置了。起碼第一個(gè)可變參數(shù)的位置應(yīng)該是:first_vararg.addr=fmt.addr+x_sizeof(fmt);根據(jù)這一結(jié)論我們?cè)囍鴮?shí)現(xiàn)一個(gè)支持可變參數(shù)的函數(shù):voidvar_args_func(constchar*fmt,...){char*ap;ap=((char*)&fmt)+sizeof(fmt);printf("%d\n",*(int*)ap);ap=ap+sizeof(int);printf("%d\n",*(int*)ap);ap=ap+sizeof(int);printf("%s\n”,*((char**)ap));}intmain(){var_args_func("%d%d%s\n",4,5,"helloworld");}輸出結(jié)果:45helloworldvar_args_func只是為了演示,并未根據(jù)fmt消息中的格式字符串來(lái)判斷變參的個(gè)數(shù)和類(lèi)型,而是直接在實(shí)現(xiàn)中寫(xiě)死了,如果你把這個(gè)程序拿到solaris9下,運(yùn)行后,一定得不到正確的結(jié)果,為什么呢,后續(xù)再說(shuō)。先來(lái)解釋一下這個(gè)程序。我們用ap獲取第一個(gè)變參的地址,我們知道第一個(gè)變參是4,一個(gè)int型,所以我們用(int*)ap以告訴編譯器,以ap為首地址的那塊內(nèi)存我們要將之視為一個(gè)整型來(lái)使用,*(int*)ap獲得該參數(shù)的值;接下來(lái)的變參是5,又一個(gè)int型,其地址是ap+sizeof(第一個(gè)變參),也就是ap+sizeof(int),同樣我們使用*(int*)ap獲得該參數(shù)的值;最后的一個(gè)參數(shù)是一個(gè)字符串,也就是char*,與前兩個(gè)int型參數(shù)不同的是,經(jīng)過(guò)ap+sizeof(int)后,ap指向棧上一個(gè)char*類(lèi)型的內(nèi)存塊(我們暫且稱(chēng)之tmp_ptr,char*tmp_ptr)的首地址,即ap->&tmp_ptr,而我們要輸出的不是printf("%s\n”,ap),而是printf("%s\n",tmp_ptr);printf("%s\n",ap)是意圖將ap所指的內(nèi)存塊作為字符串輸出了,但是ap->&tmp_ptr,tmp_ptr所占據(jù)的4個(gè)字節(jié)顯然不是字符串,而是一個(gè)地址。如何讓&tmp_ptr是char**類(lèi)型的,我們將ap進(jìn)行強(qiáng)制轉(zhuǎn)換(char**)ap<=>&tmp_ptr,這樣我們?cè)L問(wèn)tmp_ptr只需要在(char**)ap前面加上一個(gè)*即可,即printf("%s\n",*(char**)ap);前面說(shuō)過(guò),如果將var_args_func放到solaris上,一定是得不到正確結(jié)果的?為什么呢?由于內(nèi)存對(duì)齊。編譯器在棧上壓入?yún)?shù)時(shí),不是一個(gè)緊挨著另一個(gè)的,編譯器會(huì)根據(jù)變參的類(lèi)型將其放到滿(mǎn)足類(lèi)型對(duì)齊的地址上的,這樣棧上參數(shù)之間實(shí)際上可能會(huì)是有空隙的。上述例子中,我是根據(jù)反編譯后的匯編碼得到的參數(shù)間隔,還好都是4,然后在代碼中寫(xiě)死了。為了滿(mǎn)足代碼的可移植性,C標(biāo)準(zhǔn)庫(kù)在stdarg.h中提供了諸多Facilities以供實(shí)現(xiàn)變長(zhǎng)長(zhǎng)度參數(shù)時(shí)使用。這里也列出一個(gè)簡(jiǎn)單的例子,看看利用標(biāo)準(zhǔn)庫(kù)是如何支持變長(zhǎng)參數(shù)的:#include<stdarg.h>voidstd_vararg_func(constchar*fmt,...)(va_listap;va_start(ap,fmt);printf("%d\n",va_arg(ap,int));printf("%f\n",va_arg(ap,double));printf("%s\n”,va_arg(ap,char*));va_end(ap);}intmain()(std_vararg_func("%d%f%s\n",4,5.4,"helloworld");}輸出:45.400000helloworld對(duì)比一下std_vararg_func和var_args_func的實(shí)現(xiàn),va_list似乎就是char*,va_start似乎就是((char*)&fmt)+sizeof(fmt),va_arg似乎就是得到下一個(gè)參數(shù)的首地址。沒(méi)錯(cuò),多數(shù)平臺(tái)下stdarg.h中va_list,va_start和var_arg的實(shí)現(xiàn)就是類(lèi)似這樣的。一般stdarg.h會(huì)包含很多宏,看起來(lái)比較復(fù)雜。在有的系統(tǒng)中stdarg.h的實(shí)現(xiàn)依賴(lài)somespecialfunctionsbuiltintothethecompilationsystemtohandlevariableargumentlistsandstackallocations,多數(shù)其他系統(tǒng)的實(shí)現(xiàn)與下面很相似:(VisualC++6.0的實(shí)現(xiàn)較為清晰,因?yàn)閣indows上的應(yīng)用程序只需要在windows平臺(tái)間做移植即可,沒(méi)有必要考慮太多的平臺(tái)情況)。C語(yǔ)言va_list與_vsnprintf的使用先舉一個(gè)例子:#definebufsize80charbuffer[bufsize];/*這個(gè)函數(shù)用來(lái)格式化帶參數(shù)的字符串*/intvspf(char*fmt,...){va_listargptr;〃聲明一個(gè)轉(zhuǎn)換參數(shù)的變量intcnt;va_start(argptr,fmt);//初始化變量cnt=vsnprintf(buffer,bufsize,fmt,argptr);//將帶參數(shù)的字符串按照參數(shù)列表格式化到buffer中va_end(argptr);〃結(jié)束變量列表,和va_start成對(duì)使用return(cnt);}intmain(intargc,char*argv[]){intinumber=30;floatfnumber=90.0;charstring[4]="abc";vspf("%d%f%s",inumber,fnumber,string);{printf("%s\n",buffer);return0;}下面我們來(lái)探討如何寫(xiě)一個(gè)簡(jiǎn)單的可變參數(shù)的C函數(shù).寫(xiě)可變參數(shù)的C函數(shù)要在程序中用到以下這些宏:使用可變參數(shù)應(yīng)該有以下步驟:首先在函數(shù)里定義一個(gè)va_list型的變量,這里是arg_ptr,這個(gè)變量是指向參數(shù)的指針.然后用va_start宏初始化變量arg_ptr,這個(gè)宏的第二個(gè)參數(shù)是第一個(gè)可變參數(shù)的前一個(gè)參數(shù),是一個(gè)固定的參數(shù).然后用va_arg返回可變的參數(shù),并賦值給整數(shù)j.va_arg的第二個(gè)參數(shù)是你要返回的參數(shù)的類(lèi)型,這里是int型.最后用va_end宏結(jié)束可變參數(shù)的獲取.然后你就可以在函數(shù)里使用第二個(gè)參數(shù)了.如果函數(shù)有多個(gè)可變參數(shù)的,依次調(diào)用va_arg獲取各個(gè)參數(shù).如果我們用下面三種方法調(diào)用的話(huà),都是合法的,但結(jié)果卻不一樣:可變參數(shù)在編譯器中的處理我們知道va_start,va_arg,va_end是在stdarg.h中被定義成宏的,由于:硬件平臺(tái)的不同編譯器的不同MicrosoftVisualStudio\VC98\Include\stdarg.h中,typedefchar*va_list;/*把va_list被定義成char*,這是因?yàn)樵谖覀兡壳八玫腜C機(jī)上,字符指針類(lèi)型可以用來(lái)存儲(chǔ)內(nèi)存單元地址。而在有的機(jī)器上va_list是被定義成void*的*/#define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))/*_INTSIZEOF(n)宏是為了考慮那些內(nèi)存地址需要對(duì)齊的系統(tǒng),從宏的名字來(lái)應(yīng)該是跟sizeof(int)對(duì)齊。一般的sizeof(int)=4,也就是參數(shù)在內(nèi)存中的地址都為4的倍數(shù)。比如,如果sizeof(n)在1—4之間,那么_INTSIZEOF(n)=4;如果sizeof(n)在5—8之間,那么_INTSIZEOF(n)=8。*/#defineva_start(ap,v)(ap=(va_list)&v+_INTSIZEOF(v))/*va_start的定義為&v+_INTSIZEOF(v)這里&v是最后一個(gè)固定參數(shù)的起始地址,再加上其實(shí)際占用大小后,就得到了第一個(gè)可變參數(shù)的起始內(nèi)存地址。所以我們運(yùn)行va_start(ap,v)以后,ap指向第一個(gè)可變參數(shù)在的內(nèi)存地址*/#defineva_arg(ap,t)(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))/*這個(gè)宏做了兩個(gè)事情,用用戶(hù)輸入的類(lèi)型名對(duì)參數(shù)地址進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換,得到用戶(hù)所需要的值計(jì)算出本參數(shù)的實(shí)際大小,將指針調(diào)到本參數(shù)的結(jié)尾,也就是下一個(gè)參數(shù)的首地址,以便后續(xù)處理。*/#defineva_end(ap)(ap=(va_list)0)/*x86平臺(tái)定義為ap=(char*)0;使ap不再指向堆棧,而是跟NULL一樣.有些直接定義為((void*)0),這樣編譯器不會(huì)為va_end產(chǎn)生代碼,例如gcc在linux的x86平臺(tái)就是這樣定義的.在這里大家要注意一個(gè)問(wèn)題:由于參數(shù)的地址用于va_start宏,所以參數(shù)不能聲明為寄存器變量或作為函數(shù)或數(shù)組類(lèi)型.*/這里有兩個(gè)地方需要深入挖掘一下:1、#define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))我們這里簡(jiǎn)化一下這個(gè)宏:#define_INTSIZEOF(n)((sizeof(n)+x)&?(x))x=sizeof(int)-1=3=0000000000000011(b)~x=1111111111111100(b)當(dāng)一個(gè)數(shù)&(-x)時(shí),得到的值始終是sizeof(int)的倍數(shù),也就是說(shuō)_INTSIZEOF(n)的功能是將n圓整到sizeof(int)的倍數(shù)上去。sizeof(n)>=1,sizeof(n)+sizeof(int)-1經(jīng)過(guò)圓整后,一定會(huì)是>=4的整數(shù);在其他系統(tǒng)平臺(tái)上,圓整的目標(biāo)值有的是4,有的則是8,視具體系統(tǒng)而定。2、#defineva_arg(ap,t)(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))其實(shí)有了var_args_func的實(shí)現(xiàn),這里也就不難理解了。不過(guò)這里有一個(gè)trick,很多人一開(kāi)始肯定對(duì)先加上_INTSIZEOF(t),又減去_INTSIZEOF(t)很不理解,其實(shí)這里是一點(diǎn)就透的:整個(gè)表達(dá)式((ap+=_INTSIZEOF(t))-_INTSIZEOF(t))返回的值其實(shí)和最初的ap所指向的地址是一致的,關(guān)鍵就是在整個(gè)表達(dá)式被evaluated后,ap卻指向了下一個(gè)參數(shù)的地址了,就這么簡(jiǎn)單。C語(yǔ)言的函數(shù)是從右向左壓入堆棧的,圖(1)是函數(shù)的參數(shù)在堆棧中的分布位置.我們看到va_list被定義成char*,有一些平臺(tái)或操作系統(tǒng)定義為void*.再看va_start的定義,定義為&v+_INTSIZEOF(v),而&v是固定參數(shù)在堆棧的地址,所以我們運(yùn)行va_start(ap,v)以后,ap指向第一個(gè)可變參數(shù)在堆棧的地址,如圖:TOC\o"1-5"\h\z高地址I1I函數(shù)返回地址IIII…….IIII第n個(gè)參數(shù)(第一個(gè)可變參數(shù))III<--va_start后ap指向I第n-1個(gè)參數(shù)(最后一個(gè)固定參數(shù))I低地址I1<--&v圖(1)然后,我們用va_arg()取得類(lèi)型t的可變參數(shù)值,以上例為int型為例,我們看一下va_arg取int型的返回值:j=(*(int*)((ap+=_INTSIZEOF(int))-_INTSIZEOF(int)));首先ap+=sizeof(int),已經(jīng)指向下一個(gè)參數(shù)的地址了.然后返回ap-sizeof(int)的int*指針,這正是第一個(gè)可變參數(shù)在堆棧里的地址(圖2).然后用*取得這個(gè)地址的內(nèi)容(參數(shù)值)賦給j.TOC\o"1-5"\h\z高地址I1I函數(shù)返回地址IIII…….III<--va_arg后ap指向I第n個(gè)參數(shù)(第一個(gè)可變參數(shù))III<--va_start后ap指向I第n-1個(gè)參數(shù)(最后一個(gè)固定參數(shù))I低地址I1<--&v圖(2)最后要說(shuō)的是va_end宏的意思,x86平臺(tái)定義為ap=(char*)0;使ap不再指向堆棧,而是跟NULL一樣.有些直接定義為((void*)0),這樣編譯器不會(huì)為va_end產(chǎn)生代碼,例如gcc在linux的x86平臺(tái)就是這樣定義的.在這里大家要注意一個(gè)問(wèn)題:由于參數(shù)的地址用于va_start宏,所以參數(shù)不能聲明為寄存器變量或作為函數(shù)或數(shù)組類(lèi)型.關(guān)于va_start,va_arg,va_end的描述就是這些了,我們要注意的是不同的操作系統(tǒng)和硬件平臺(tái)的定義有些不同,但原理卻是相似的.可變參數(shù)在編程中要注意的問(wèn)題因?yàn)関a_start,va_arg,va_end等定義成宏,所以它顯得很愚蠢,可變參數(shù)的類(lèi)型和個(gè)數(shù)完全在該函數(shù)中由程序代碼控制,它并不能智能地識(shí)別不同參數(shù)的個(gè)數(shù)和類(lèi)型.有人會(huì)問(wèn):那么printf中不是實(shí)現(xiàn)了智能識(shí)別參數(shù)嗎?那是因?yàn)楹瘮?shù)printf是從固定參數(shù)format字符串來(lái)分析出參數(shù)的類(lèi)型,再調(diào)用va_arg的來(lái)獲取可變參數(shù)的.也就是說(shuō),你想實(shí)現(xiàn)智能識(shí)別可變參數(shù)的話(huà)是要通過(guò)在自己的程序里作判斷來(lái)實(shí)現(xiàn)的.另外有一個(gè)問(wèn)題,因?yàn)榫幾g器對(duì)可變參數(shù)的函數(shù)的原型檢查不夠嚴(yán)格,對(duì)編程查錯(cuò)不利.如果simple_va_fun()改為:voidsimple_va_fun(inti,...){va_listarg_ptr;char*s=NULL;va_start(arg_ptr,i);s=va_arg(arg_ptr,char*);va_end(arg_ptr);printf("%d%s\n”,i,s);return0;}可變參數(shù)為char*型,當(dāng)我們忘記用兩個(gè)參數(shù)來(lái)調(diào)用該函數(shù)時(shí),就會(huì)出現(xiàn)coredump(Unix)或者頁(yè)面非法的錯(cuò)誤(window平臺(tái)).但也有可能不出錯(cuò),但錯(cuò)誤卻是難以發(fā)現(xiàn),不利于我們寫(xiě)出高質(zhì)量的程序.以下提一下va系列宏的兼容性.SystemVUnix把va_start定義為只有一個(gè)參數(shù)的宏:va_start(va_listarg_ptr);而ANSIC則定義為:va_start(va_listarg_ptr,prev_param);如果我們要用systemV的定義,應(yīng)該用vararg.h頭文件中所定義的宏,ANSIC的宏跟systemV的宏是不兼容的,我們一般都用ANSIC,所以用ANSIC的定義就夠了,也便于程序的移植.小結(jié):可變參數(shù)的函數(shù)原理其實(shí)很簡(jiǎn)單,而va系列是以宏定義來(lái)定義的,實(shí)現(xiàn)跟堆棧相關(guān).我們寫(xiě)一個(gè)可變函數(shù)的C

溫馨提示

  • 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶(hù)所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶(hù)上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶(hù)上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶(hù)因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論