在這里開(kāi)始書寫日記,、心情 …很多技術(shù)人員都有在"技術(shù)細(xì)節(jié)"上"鉆牛角尖"的"癖好",對(duì)此很多人褒貶不一,;無(wú)論怎樣,,我也是屬于這類人。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ù),, void fixed_args_func(int a, double b, 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ù)的名稱,、類型都是直接可見(jiàn)的,,他們的地址也都是可以直接得到的,比如:通過(guò)&a我們可以得到a的地址,,并通過(guò)函數(shù) 原型聲明了解到a是int類型的; 通過(guò)&b我們可以得到b的地址,,并通過(guò)函數(shù)原型聲明了解到b是double類型的; 通過(guò)&c我們可以得到c的地址,并通過(guò)函數(shù)原型聲明了解到c是char*類型的,。 但是對(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ù)的地址,,比如: void var_args_func(const char * fmt, ... ) { ... ... } 這 里我們只能得到fmt這固定參數(shù)的地址,,僅從函數(shù)原型我們是無(wú)法確定"..."中有幾個(gè)參數(shù)、參數(shù)都是什么類型的,自然也就無(wú)法確定其位置了,。那么如何可 以做到呢,?在大腦中回想一下函數(shù)傳參的過(guò)程,無(wú)論"..."中有多少個(gè)參數(shù),、每個(gè)參數(shù)是什么類型的,,它們都和固定參數(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,,Windows XP, MinGW gcc v3.4.2下成立) 我們先用上面的那個(gè)fixed_args_func函數(shù)確定一下這個(gè)平臺(tái)下的入棧順序,。 int main() { fixed_args_func(17, 5.40, "hello world"); return 0; } a = 0x0022FF50 b = 0x0022FF54 c = 0x0022FF5C 從這個(gè)結(jié)果來(lái)看,,顯然參數(shù)是從右到左,逐一壓入棧中的(棧的延伸方向是從高地址到低地址,,棧底的占領(lǐng)著最高內(nèi)存地址,,先入棧的參數(shù),其地理位置也就最高了),。我們基本可以得出這樣一個(gè)結(jié)論: c.addr = b.addr + x_sizeof(b); /*注意: x_sizeof != sizeof,,后話再說(shuō) */ b.addr = a.addr + x_sizeof(a); 有 了以上的"等式",我們似乎可以推導(dǎo)出 void var_args_func(const char * fmt, ... ) 函數(shù)中,,可變參數(shù)的位置了,。起碼第一個(gè)可變參數(shù)的位置應(yīng)該是:first_vararg.addr = fmt.addr + x_sizeof(fmt); 根據(jù)這一結(jié)論我們?cè)囍鴮?shí)現(xiàn)一個(gè)支持可變參數(shù)的函數(shù): void var_args_func(const char * 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)); } int main(){ var_args_func("%d %d %s\n", 4, 5, "hello world"); } 輸出結(jié)果: 4 5 hello world var_args_func 只是為了演示,并未根據(jù)fmt消息中的格式字符串來(lái)判斷變參的個(gè)數(shù)和類型,,而是直接在實(shí)現(xiàn)中寫死了,,如果你把這個(gè)程序拿到solaris 9下,運(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*類型的內(nèi)存塊(我們暫且稱之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 **類型的,我們將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ù)變參的類型將其放到滿足類型對(duì)齊的地址上的,,這樣棧上參數(shù)之間實(shí)際上可能會(huì)是有空隙的。上述例子中,,我是根據(jù)反編譯后的匯編碼得到的參數(shù)間隔,,還好都是4,然后在代碼中寫死了,。 為了滿足代碼的可移植性,,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> void std_vararg_func(const char *fmt, ... ) { va_list ap; 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); } int main() { std_vararg_func("%d %f %s\n", 4, 5.4, "hello world"); } 輸出: 4 5.400000 hello world 對(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)就是類似這樣的。一般stdarg.h會(huì)包含很多宏,,看起來(lái)比較復(fù)雜,。在有的系統(tǒng)中stdarg.h的實(shí)現(xiàn)依賴 some special functions built into the the compilation system to handle variable argument lists and stack allocations,多數(shù)其他系統(tǒng)的實(shí)現(xiàn)與下面很相似:(Visual C++ 6.0的實(shí)現(xiàn)較為清晰,,因?yàn)閣indows上的應(yīng)用程序只需要在windows平臺(tái)間做移植即可,,沒(méi)有必要考慮太多的平臺(tái)情況)。 Microsoft Visual Studio\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 ) 這里有兩個(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 = 0000 0000 0000 0011(b) ~x = 1111 1111 1111 1100(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、#define va_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)單 |
|
來(lái)自: oskycar > 《c\vc\opencv》