c語言中可變參數(shù)函數(shù)的設(shè)計(jì)
1,首先,怎么得到參數(shù)的值。對于一般的函數(shù),我們可以通過參數(shù)對應(yīng)在參數(shù)列表里的標(biāo)識符來得到。但是參數(shù)可變函數(shù)那些可變的參數(shù)是沒有參數(shù)標(biāo)識符的,它只有“…”,所以通過標(biāo)識符來得到是不可能的,我們只有另辟途徑。我們知道函數(shù)調(diào)用時都會分配棧空間,而函數(shù)調(diào)用機(jī)制中的棧結(jié)構(gòu)如下圖所示:
| ...... |
------------------
| 參數(shù)2 |
------------------
| 參數(shù)1 |
------------------
| 返回地址 |
------------------
|調(diào)用函數(shù)運(yùn)行狀態(tài)|
------------------
可見,參數(shù)是連續(xù)存儲在棧里面的,那么也就是說,我們只要得到可變參數(shù)的前一個參數(shù)的地址,就可以通過指針訪問到那些可變參數(shù)。但是怎么樣得到可變參數(shù)的前一個參數(shù)的地址呢?不知道你注意到?jīng)]有,參數(shù)可變函數(shù)在可變參數(shù)之前必有一個參數(shù)是固定的,并使用標(biāo)識符,而且通常被聲明為char*類型,printf函數(shù)也不例外。這樣的話,我們就可以通過這個參數(shù)對應(yīng)的標(biāo)識符來得到地址,從而訪問其他參數(shù)變得可能。我們可以寫一個測試程序來試一下:
#include <stdio.h>
void va_test(char* fmt,...);//參數(shù)可變的函數(shù)聲明
void main()
{
int a=1,c=55;
char b='b';
va_test("",a,b,c);//用四個參數(shù)做測試
}
void va_test(char* fmt,...) //參數(shù)可變的函數(shù)定義,注意第一個參數(shù)為char* fmt
{
char *p=NULL;
p=(char *)&fmt;//注意不是指向fmt,而是指向&fmt,并且強(qiáng)制轉(zhuǎn)化為char *,以便一個一個字節(jié)訪問
for(int i = 0;i<16;i++)//16是通過計(jì)算的值(參數(shù)個數(shù)*4個字節(jié)),只是為了測試,暫且將就一下
{
printf("%.4d ",*p);//輸出p指針指向地址的值
p++;
}
}
編譯運(yùn)行的結(jié)果為
0056 0000 0066 0000 | 0001 0000 0000 0000 | 0098 0000 0000 0000 | 0055 0000 0000 0000
由運(yùn)行結(jié)果可見,通過這樣方式可以逐一獲得可變參數(shù)的值。
至于為什么通常被聲明為char*類型,我們慢慢看來。
2,怎樣確定參數(shù)類型和數(shù)量
通過上述的方式,我們首先解決了取得可變參數(shù)值的問題,但是對于一個參數(shù),值很重要,其類型同樣舉足輕重,而對于一個函數(shù)來講參數(shù)個數(shù)也非常重要,否則就會產(chǎn)生了一系列的麻煩來。通過訪問存儲參數(shù)的棧空間,我們并不能得到關(guān)于類型的任何信息和參數(shù)個數(shù)的任何信息。我想你應(yīng)該想到了——使用char *參數(shù)。Printf函數(shù)就是這樣實(shí)現(xiàn)的,它把后面的可變參數(shù)類型都放到了char *指向的字符數(shù)組里,并通過%來標(biāo)識以便與其它的字符相區(qū)別,從而確定了參數(shù)類型也確定了參數(shù)個數(shù)。其實(shí),用何種方式來到達(dá)這樣的效果取決于函數(shù)的實(shí)現(xiàn)。比如說,定義一個函數(shù),預(yù)知它的可變參數(shù)類型都是int,那么固定參數(shù)完全可以用int類型來替換char*類型,因?yàn)橹灰玫絽?shù)個數(shù)就可以了。
3,言歸正傳
我想到了這里,大概的輪廓已經(jīng)呈現(xiàn)出來了。本來想就此作罷的(我的惰性使然),但是一想到如果不具實(shí)用性便可能是一堆廢物,枉費(fèi)我打了這么些字,決定還是繼續(xù)下去。
我是比較抵制用那些不明所以的宏定義的,所以在上面的闡述里一點(diǎn)都沒有涉及定義在<stdarg.h>的va(variable-argument)宏。事實(shí)上,當(dāng)時讓我產(chǎn)生極大疑惑和好奇的正是這幾個宏定義。但是現(xiàn)在我們不得不要去和這些宏定義打打交道,畢竟我們在討生計(jì)的時候還得用上他們,這也是我曰之為“言歸正傳”的理由。
好了,我們來看一下那些宏定義。
打開<stdarg.h>文件,找一下va_*的宏定義,發(fā)現(xiàn)不單單只有一組,但是在各組定義前都會有宏編譯。宏編譯指示的是不同硬件平臺和編譯器下用怎樣的va宏定義。比較一下,不同之處主要在偏移量的計(jì)算上。我們還是拿個典型又熟悉的——X86的相關(guān)宏定義:
1)typedef char * va_list;
2)#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
3)#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
4)#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
5)#define va_end(ap) ( ap = (va_list)0 )
我們逐一看來:
第一個我想不必說了,類型定義罷了。第二個是頗有些來頭的,我們也不得不搞懂它,因?yàn)楹竺娴膬蓚€關(guān)鍵的宏定義都用到了。不知道你夠不夠細(xì)心,有沒有發(fā)現(xiàn)在上面的測試程序中,第二個可變參數(shù)明明是char類型,可是在輸出結(jié)果中占了4個byte。難道所有的參數(shù)都會占4個byte的空間?那如果是double類型的參數(shù),且不是會丟失數(shù)據(jù)!如果你不嫌麻煩的話,再去做個測試吧,在上面的測試程序中用一個double類型(長度為8byte)和一個long double類型(長度為10byte)做可變參數(shù)。發(fā)現(xiàn)什么?double類型占了8byte,而long double占了12byte。好像都是4的整數(shù)倍哦。不得不引出另一個概念了“對齊(alignment)”,所謂對齊,對Intel80x86 機(jī)器來說就是要求每個變量的地址都是sizeof(int)的倍數(shù)。原來我們搞錯了,char類型的參數(shù)只占了1byte,但是它后面的參數(shù)因?yàn)閷R的關(guān)系只能跳過3byte存儲,而那3byte也就浪費(fèi)掉了。那為什么要對齊?因?yàn)樵趯R方式下,CPU 的運(yùn)行效率要快得多(舉個例子吧,要說明的是下面的例子是我從網(wǎng)上摘錄下來的,不記得出處了。
示例:如下圖,當(dāng)一個long 型數(shù)(如圖中l(wèi)ong1)在內(nèi)存中的位置正好與內(nèi)存的字邊界對齊時,CPU 存取這個數(shù)只需訪問一次內(nèi)存,而當(dāng)一個long 型數(shù)(如圖中的long2)在內(nèi)存中的位置跨越了字邊界時,CPU 存取這個數(shù)就需要多次訪問內(nèi)存,如i960cx 訪問這樣的數(shù)需讀內(nèi)存三次(一個BYTE、一個SHORT、一個BYTE,由CPU 的微代碼執(zhí)行,對軟件透明),所以對齊方式下CPU 的運(yùn)行效率明顯快多了。
1 8 16 24 32
------- ------- ------- ---------
| long1 | long1 | long1 | long1 |
------- ------- ------- ---------
| | | | long2 |
------- ------- ------- ---------
| long2 | long2 | long2 | |
------- ------- ------- ---------
| ....)。好像扯得有點(diǎn)遠(yuǎn)來,但是有助于對_INTSIZEOF(n)的理解。位操作對于我來說是玄的東東。單個位運(yùn)算還應(yīng)付得來,而這樣一個表達(dá)式擺在面前就暈了。怎么辦?菜鳥自有菜的辦法。(待續(xù))
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=14721
---------------------------------------------------------------------------------------------------------------------
C語言中的可變參數(shù)函數(shù) CSDN Blog推出文章指數(shù)概念,文章指數(shù)是對Blog文章綜合評分后推算出的,綜合評分項(xiàng)分別是該文章的點(diǎn)擊量,回復(fù)次數(shù),被網(wǎng)摘收錄數(shù)量,文章長度和文章類型;滿分100,每月更新一次。
第一篇
C語言編程中有時會遇到一些參數(shù)個數(shù)可變的函數(shù),例如printf()函數(shù),其函數(shù)原型為:
int printf( const char* format, ...);
它除了有一個參數(shù)format固定以外,后面跟的參數(shù)的個數(shù)和類型是可變的(用三個點(diǎn)“…”做參數(shù)占位符),實(shí)際調(diào)用時可以有以下的形式:
printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
一個簡單的可變參數(shù)的C函數(shù)
先看例子程序。該函數(shù)至少有一個整數(shù)參數(shù),其后占位符…,表示后面參數(shù)的個數(shù)不定。在這個例子里,所有的輸入?yún)?shù)必須都是整數(shù),函數(shù)的功能只是打印所有參數(shù)的值。函數(shù)代碼如下:
//示例代碼1:可變參數(shù)函數(shù)的使用
#include "stdio.h"
#include "stdarg.h"
void simple_va_fun(int start, ...)
{
va_list arg_ptr;
int nArgValue =start;
int nArgCout="0"; //可變參數(shù)的數(shù)目
va_start(arg_ptr,start); //以固定參數(shù)的地址為起點(diǎn)確定變參的內(nèi)存起始地址。
do
{
++nArgCout;
printf("the %d th arg: %d",nArgCout,nArgValue); //輸出各參數(shù)的值
nArgValue = va_arg(arg_ptr,int); //得到下一個可變參數(shù)的值
} while(nArgValue != -1);
return;
}
int main(int argc, char* argv[])
{
simple_va_fun(100,-1);
simple_va_fun(100,200,-1);
return 0;
}
下面解釋一下這些代碼。從這個函數(shù)的實(shí)現(xiàn)可以看到,我們使用可變參數(shù)應(yīng)該有以下步驟:
⑴由于在程序中將用到以下這些宏:
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(可變參數(shù))的意思。
這些宏定義在stdarg.h中,所以用到可變參數(shù)的程序應(yīng)該包含這個頭文件。
⑵函數(shù)里首先定義一個va_list型的變量,這里是arg_ptr,這個變量是存儲參數(shù)地址的指針.因?yàn)榈玫絽?shù)的地址之后,再結(jié)合參數(shù)的類型,才能得到參數(shù)的值。
⑶然后用va_start宏初始化⑵中定義的變量arg_ptr,這個宏的第二個參數(shù)是可變參數(shù)列表的前一個參數(shù),即最后一個固定參數(shù)。
⑷然后依次用va_arg宏使arg_ptr返回可變參數(shù)的地址,得到這個地址之后,結(jié)合參數(shù)的類型,就可以得到參數(shù)的值。
⑸設(shè)定結(jié)束條件,這里的條件就是判斷參數(shù)值是否為-1。注意被調(diào)的函數(shù)在調(diào)用時是不知道可變參數(shù)的正確數(shù)目的,程序員必須自己在代碼中指明結(jié)束條件。至于為什么它不會知道參數(shù)的數(shù)目,在看完這幾個宏的內(nèi)部實(shí)現(xiàn)機(jī)制后,自然就會明白。
第二篇
C語言之可變參數(shù)問題
C語言中有一種長度不確定的參數(shù),形如:"…",它主要用在參數(shù)個數(shù)不確定的函數(shù)中,我們最容易想到的例子是printf函數(shù)。
原型:
int printf( const char *format [, argument]... );
使用例:
printf("Enjoy yourself everyday!\n");
printf("The value is %d!\n", value);
這種可變參數(shù)可以說是C語言一個比較難理解的部分,這里會由幾個問題引發(fā)一些對它的分析。
注意:在C++中有函數(shù)重載(overload)可以用來區(qū)別不同函數(shù)參數(shù)的調(diào)用,但它還是不能表示任意數(shù)量的函數(shù)參數(shù)。
問題:printf的實(shí)現(xiàn)
請問,如何自己實(shí)現(xiàn)printf函數(shù),如何處理其中的可變參數(shù)問題? 答案與分析:
在標(biāo)準(zhǔn)C語言中定義了一個頭文件專門用來對付可變參數(shù)列表,它包含了一組宏,和一個va_list的typedef聲明。一個典型實(shí)現(xiàn)如下:
typedef char* va_list;
#define va_start(list) list = (char*)&va_alist
#define va_end(list)
#define va_arg(list, mode)\
((mode*) (list += sizeof(mode)))[-1]
自己實(shí)現(xiàn)printf:
#include
int printf(char* format, …)
{
va_list ap;
va_start(ap, format);
int n = vprintf(format, ap);
va_end(ap);
return n;
}
問題:運(yùn)行時才確定的參數(shù)
有沒有辦法寫一個函數(shù),這個函數(shù)參數(shù)的具體形式可以在運(yùn)行時才確定?
答案與分析:
目前沒有"正規(guī)"的解決辦法,不過獨(dú)門偏方倒是有一個,因?yàn)橛幸粋€函數(shù)已經(jīng)給我們做出了這方面的榜樣,那就是main(),它的原型是:
int main(int argc,char *argv[]);
函數(shù)的參數(shù)是argc和argv。
深入想一下,"只能在運(yùn)行時確定參數(shù)形式",也就是說你沒辦法從聲明中看到所接受的參數(shù),也即是參數(shù)根本就沒有固定的形式。常用的辦法是你可以通過定義一個void *類型的參數(shù),用它來指向?qū)嶋H的參數(shù)區(qū),然后在函數(shù)中根據(jù)根據(jù)需要任意解釋它們的含義。這就是main函數(shù)中argv的含義,而argc,則用來表明實(shí)際的參數(shù)個數(shù),這為我們使用提供了進(jìn)一步的方便,當(dāng)然,這個參數(shù)不是必需的。
雖然參數(shù)沒有固定形式,但我們必然要在函數(shù)中解析參數(shù)的意義,因此,理所當(dāng)然會有一個要求,就是調(diào)用者和被調(diào)者之間要對參數(shù)區(qū)內(nèi)容的格式,大小,有效性等所有方面達(dá)成一致,否則南轅北轍各說各話就慘了。
問題:可變長參數(shù)的傳遞
有時候,需要編寫一個函數(shù),將它的可變長參數(shù)直接傳遞給另外的函數(shù),請問,這個要求能否實(shí)現(xiàn)?
答案與分析:
目前,你尚無辦法直接做到這一點(diǎn),但是我們可以迂回前進(jìn),首先,我們定義被調(diào)用函數(shù)的參數(shù)為va_list類型,同時在調(diào)用函數(shù)中將可變長參數(shù)列表轉(zhuǎn)換為va_list,這樣就可以進(jìn)行變長參數(shù)的傳遞了。看如下所示:
void subfunc (char *fmt, va_list argp)
{
...
arg = va_arg (fmt, argp); /* 從argp中逐一取出所要的參數(shù) */
...
}
void mainfunc (char *fmt, ...)
{
va_list argp;
va_start (argp, fmt); /* 將可變長參數(shù)轉(zhuǎn)換為va_list */
subfunc (fmt, argp); /* 將va_list傳遞給子函數(shù) */
va_end (argp);
...
}
問題:可變長參數(shù)中類型為函數(shù)指針
我想使用va_arg來提取出可變長參數(shù)中類型為函數(shù)指針的參數(shù),結(jié)果卻總是不正確,為什么?
答案與分析:
這個與va_arg的實(shí)現(xiàn)有關(guān)。一個簡單的、演示版的va_arg實(shí)現(xiàn)如下:
#define va_arg(argp, type) \
(*(type *)(((argp) += sizeof(type)) - sizeof(type)))
其中,argp的類型是char *。
如果你想用va_arg從可變參數(shù)列表中提取出函數(shù)指針類型的參數(shù),例如
int (*)(),則va_arg(argp, int (*)())被擴(kuò)展為:
(*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))
顯然,(int (*)() *)是無意義的。
解決這個問題的辦法是將函數(shù)指針用typedef定義成一個獨(dú)立的數(shù)據(jù)類型,例如:
typedef int (*funcptr)();
這時候再調(diào)用va_arg(argp, funcptr)將被擴(kuò)展為:
(* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))
這樣就可以通過編譯檢查了。
問題:可變長參數(shù)的獲取
有這樣一個具有可變長參數(shù)的函數(shù),其中有下列代碼用來獲取類型為float的實(shí)參:
va_arg (argp, float);
這樣做可以嗎?
答案與分析:
不可以。在可變長參數(shù)中,應(yīng)用的是"加寬"原則。也就是float類型被擴(kuò)展成double;char, short被擴(kuò)展成int。因此,如果你要去可變長參數(shù)列表中原來為float類型的參數(shù),需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。
問題:定義可變長參數(shù)的一個限制
為什么我的編譯器不允許我定義如下的函數(shù),也就是可變長參數(shù),但是沒有任何的固定參數(shù)?
int f (...)
{
...
}
答案與分析:
不可以。這是ANSI C 所要求的,你至少得定義一個固定參數(shù)。
這個參數(shù)將被傳遞給va_start(),然后用va_arg()和va_end()來確定所有實(shí)際調(diào)用時可變長參數(shù)的類型和值。---------------------------------------------------------------------------------------------------------------------
如何判別可變參數(shù)函數(shù)的參數(shù)類型?
函數(shù)形式如下:
void fun(char* str,...)
{
......
}
若傳的參數(shù)個數(shù)大于1,如何判別第2個以后傳參的參數(shù)類型???
最好有源碼說明!
沒辦法判斷的
如樓上所說,例如printf( "%d%c%s ", ....)是通過格式串中的%d, %c, %s來確定后面參數(shù)的類型,其實(shí)你也可以參考這種方法來判斷不定參數(shù)的類型.
無法判斷。可變參數(shù)實(shí)現(xiàn)主要通過三個宏實(shí)現(xiàn):va_start, va_arg, va_end。
六、 擴(kuò)展與思考
個數(shù)可變參數(shù)在聲明時只需"..."即可;但是,我們在接受這些參數(shù)時不能"..."。va函數(shù)實(shí)現(xiàn)的關(guān)鍵就是如何得到參數(shù)列表中可選參數(shù),包括參數(shù)的值和類型。以上的所有實(shí)現(xiàn)都是基于來自stdarg.h的va_xxx的宏定義。 <思考>能不能不借助于va_xxx,自己實(shí)現(xiàn)VA呢?,我想到的方法是匯編。在C中,我們當(dāng)然就用C的嵌入?yún)R編來實(shí)現(xiàn),這應(yīng)該是可以做得到的。至于能做到什么程度,穩(wěn)定性和效率怎么樣,主要要看你對內(nèi)存和指針的控制了。
參考資料
1.IEEE和OpenGroup聯(lián)合開發(fā)的Single Unix specification Ver3;BR>
2.Linux man手冊;
3.x86匯編,還有一些安全編碼方面的資料。
---------------------------------------------------------------------------------------------------------------------
[轉(zhuǎn)帖]對C/C++可變參數(shù)表的深層探索
C/C++語言有一個不同于其它語言的特性,即其支持可變參數(shù),典型的函數(shù)如printf、scanf等可以接受數(shù)量不定的參數(shù)。如:
printf ( "I love you" );
printf ( "%d", a );
printf ( "%d,%d", a, b );
第一、二、三個printf分別接受1、2、3個參數(shù),讓我們看看printf函數(shù)的原型:
int printf ( const char *format, ... );
從函數(shù)原型可以看出,其除了接收一個固定的參數(shù)format以外,后面的參數(shù)用"…"表示。在C/C++語言中,"…"表示可以接受不定數(shù)量的參數(shù),理論上來講,可以是0或0以上的n個參數(shù)。
本文將對C/C++可變參數(shù)表的使用方法及C/C++支持可變參數(shù)表的深層機(jī)理進(jìn)行探索。
一. 可變參數(shù)表的用法
1、相關(guān)宏
標(biāo)準(zhǔn)C/C++包含頭文件stdarg.h,該頭文件中定義了如下三個宏:
void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */
type va_arg ( va_list arg_ptr, type );
void va_end ( va_list arg_ptr );
在這些宏中,va就是variable argument(可變參數(shù))的意思;arg_ptr是指向可變參數(shù)表的指針;prev_param則指可變參數(shù)表的前一個固定參數(shù);type為可變參數(shù)的類型。va_list也是一個宏,其定義為typedef char * va_list,實(shí)質(zhì)上是一 char型指針。char型指針的特點(diǎn)是++、--操作對其作用的結(jié)果是增1和減1(因?yàn)閟izeof(char)為1),與之不同的是int等其它類型指針的++、--操作對其作用的結(jié)果是增sizeof(type)或減sizeof(type),而且sizeof (type)大于1。
通過va_start宏我們可以取得可變參數(shù)表的首指針,這個宏的定義為:
#define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) )
顯而易見,其含義為將最后那個固定參數(shù)的地址加上可變參數(shù)對其的偏移后賦值給ap,這樣ap就是可變參數(shù)表的首地址。其中的_INTSIZEOF宏定義為:
#define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) - 1 ) & ~( sizeof( int ) - 1 ) )
va_arg宏的意思則指取出當(dāng)前arg_ptr所指的可變參數(shù)并將ap指針指向下一可變參數(shù),其原型為:
#define va_arg(list, mode) ((mode *)(list =(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]
對這個宏的具體含義我們將在后面深入討論。
而va_end宏被用來結(jié)束可變參數(shù)的獲取,其定義為:
#define va_end ( list )
可以看出,va_end ( list )實(shí)際上被定義為空,沒有任何真實(shí)對應(yīng)的代碼,用于代碼對稱,與va_start對應(yīng);另外,它還可能發(fā)揮代碼的"自注釋"作用。所謂代碼的"自注釋",指的是代碼能自己注釋自己。
下面我們以具體的例子來說明以上三個宏的使用方法。
2、一個簡單的例子
#include <stdarg.h>
/* 函數(shù)名:max
* 功能:返回n個整數(shù)中的最大值
* 參數(shù):num:整數(shù)的個數(shù) ...:num個輸入的整數(shù)
* 返回值:求得的最大整數(shù)
*/
int max ( int num, ... )
{
int m = -0x7FFFFFFF; /* 32系統(tǒng)中最小的整數(shù) */
va_list ap;
va_start ( ap, num );
for ( int i= 0; i< num; i++ )
{
int t = va_arg (ap, int);
if ( t > m )
{
m = t;
}
}
va_end (ap);
return m;
}
/* 主函數(shù)調(diào)用max */
int main ( int argc, char* argv[] )
{
int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求5個整數(shù)中的最大值 */
cout << n;
return 0;
}
函數(shù)max中首先定義了可變參數(shù)表指針ap,而后通過va_start ( ap, num )取得了參數(shù)表首地址(賦給了ap),其后的for循環(huán)則用來遍歷可變參數(shù)表。這種遍歷方式與我們在數(shù)據(jù)結(jié)構(gòu)教材中經(jīng)常看到的遍歷方式是類似的。
函數(shù)max看起來簡潔明了,但是實(shí)際上printf的實(shí)現(xiàn)卻遠(yuǎn)比這復(fù)雜。max函數(shù)之所以看起來簡單,是因?yàn)椋?br> (1) max函數(shù)可變參數(shù)表的長度是已知的,通過num參數(shù)傳入;
(2) max函數(shù)可變參數(shù)表中參數(shù)的類型是已知的,都為int型。
而printf函數(shù)則沒有這么幸運(yùn)。首先,printf函數(shù)可變參數(shù)的個數(shù)不能輕易的得到,而可變參數(shù)的類型也不是固定的,需由格式字符串進(jìn)行識別(由%f、%d、%s等確定),因此則涉及到可變參數(shù)表的更復(fù)雜應(yīng)用。
下面我們以實(shí)例來分析可變參數(shù)表的高級應(yīng)用。
二. 高級應(yīng)用
下面這個程序是我們?yōu)槟城度胧较到y(tǒng)(該系統(tǒng)中CPU的字長為16位)編寫的在屏幕上顯示格式字符串的函數(shù)DrawText,它的用法類似于 int printf ( const char *format, ... )函數(shù),但其輸出的目標(biāo)為嵌入式系統(tǒng)的液晶顯示屏幕(LED)。
///////////////////////////////////////////////////////////////////////////////
// 函數(shù)名稱: DrawText
// 功能說明: 在顯示屏上繪制文字
// 參數(shù)說明: xPos ---橫坐標(biāo)的位置 [0 .. 30]
// yPos ---縱坐標(biāo)的位置 [0 .. 64]
// ... 可以同數(shù)字一起顯示,需設(shè)置標(biāo)志(%d、%l、%x、%s)
///////////////////////////////////////////////////////////////////////////////
extern void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )
{
BYTE lpData[100]; //緩沖區(qū)
BYTE byIndex;
BYTE byLen;
DWORD dwTemp;
WORD wTemp;
int i;
va_list lpParam;
memset( lpData, 0, 100);
byLen = strlen( lpStr );
byIndex = 0;
va_start ( lpParam, lpStr );
for ( i = 0; i < byLen; i++ )
{
if( lpStr[i] != ’%’ ) //不是格式符開始
{
lpData[byIndex++] = lpStr[i];
}
else
{
switch (lpStr[i+1])
{
//整型
case ’d’:
case ’D’:
wTemp = va_arg ( lpParam, int );
byIndex += IntToStr( lpData+byIndex, (DWORD)wTemp );
i++;
break;
//長整型
case ’l’:
case ’L’:
dwTemp = va_arg ( lpParam, long );
byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp );
i++;
break;
//16進(jìn)制(長整型)
case ’x’:
case ’X’:
dwTemp = va_arg ( lpParam, long );
byIndex += HexToStr ( lpData+byIndex, (DWORD)dwTemp );
i++;
break;
default:
lpData[byIndex++] = lpStr[i];
break;
}
}
}
va_end ( lpParam );
lpData[byIndex] = ’#CONTENT#’;
DisplayString ( xPos, yPos, lpData, TRUE); //在屏幕上顯示字符串lpData
}
在這個函數(shù)中,需通過對傳入的格式字符串(首地址為lpStr)進(jìn)行識別來獲知可變參數(shù)個數(shù)及各個可變參數(shù)的類型,具體實(shí)現(xiàn)體現(xiàn)在for循環(huán)中。譬如,在識別為%d后,做的是va_arg ( lpParam, int ),而獲知為%l和%x后則進(jìn)行的是va_arg ( lpParam, long )。格式字符串識別完成后,可變參數(shù)也就處理完了。
在項(xiàng)目的最初,我們一直苦于不能找到一個好的辦法來混合輸出字符串和數(shù)字,我們采用了分別顯示數(shù)字和字符串的方法,并分別指定坐標(biāo),程序條理被破壞。而且,在混合顯示的時候,要給各類數(shù)據(jù)分別人工計(jì)算坐標(biāo),我們感覺頭疼不已。以前的函數(shù)為:
//顯示字符串
showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr )
//顯示數(shù)字
showNum ( BYTE xPos, BYTE yPos, int num )
//以16進(jìn)制方式顯示數(shù)字
showHexNum ( BYTE xPos, BYTE yPos, int num )
最終,我們用DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )函數(shù)代替了原先所有的輸出函數(shù),程序得到了簡化。就這樣,兄弟們用得爽翻了。
三. 運(yùn)行機(jī)制探索
通過第2節(jié)我們學(xué)會了可變參數(shù)表的使用方法,相信喜歡拋根問底的讀者還不甘心,必然想知道如下問題:
(1)為什么按照第2節(jié)的做法就可以獲得可變參數(shù)并對其進(jìn)行操作?
(2)C/C++在底層究竟是依靠什么來對這一語法進(jìn)行支持的,為什么其它語言就不能提供可變參數(shù)表呢?
我們帶著這些疑問來一步步進(jìn)行摸索。
3.1 調(diào)用機(jī)制反匯編
反匯編是研究語法深層特性的終極良策,先來看看2.2節(jié)例子中主函數(shù)進(jìn)行max ( 5, 5, 6 ,3 ,8 ,5)調(diào)用時的反匯編:
1. 004010C8 push 5
2. 004010CA push 8
3. 004010CC push 3
4. 004010CE push 6
5. 004010D0 push 5
6. 004010D2 push 5
7. 004010D4 call @ILT+5(max) (0040100a)
從上述反匯編代碼中我們可以看出,C/C++函數(shù)調(diào)用的過程中:
第一步:將參數(shù)從右向左入棧(第1~6行);
第二步:調(diào)用call指令進(jìn)行跳轉(zhuǎn)(第7行)。
這兩步包含了深刻的含義,它說明C/C++默認(rèn)的調(diào)用方式為由調(diào)用者管理參數(shù)入棧的操作,且入棧的順序?yàn)閺挠抑磷螅@種調(diào)用方式稱為_cdecl調(diào)用。x86系統(tǒng)的入棧方向?yàn)閺母叩刂返降偷刂罚实?至n個參數(shù)被放在了地址遞增的堆棧內(nèi)。在被調(diào)用函數(shù)內(nèi)部,讀取這些堆棧的內(nèi)容就可獲得各個參數(shù)的值,讓我們反匯編到max函數(shù)的內(nèi)部:
int max ( int num, ...)
{
1. 00401020 push ebp
2. 00401021 mov ebp,esp
3. 00401023 sub esp,50h
4. 00401026 push ebx
5. 00401027 push esi
6. 00401028 push edi
7. 00401029 lea edi,[ebp-50h]
8. 0040102C mov ecx,14h
9. 00401031 mov eax,0CCCCCCCCh
10. 00401036 rep stos dword ptr [edi]
va_list ap;
int m = -0x7FFFFFFF; /* 32系統(tǒng)中最小的整數(shù) */
11. 00401038 mov dword ptr [ebp-8],80000001h
va_start ( ap, num );
12. 0040103F lea eax,[ebp+0Ch]
13. 00401042 mov dword ptr [ebp-4],eax
for ( int i= 0; i< num; i++ )
14. 00401045 mov dword ptr [ebp-0Ch],0
15. 0040104C jmp max+37h (00401057)
16. 0040104E mov ecx,dword ptr [ebp-0Ch]
17. 00401051 add ecx,1
18. 00401054 mov dword ptr [ebp-0Ch],ecx
19. 00401057 mov edx,dword ptr [ebp-0Ch]
20. 0040105A cmp edx,dword ptr [ebp+8]
21. 0040105D jge max+61h (00401081)
{
int t= va_arg (ap, int);
22. 0040105F mov eax,dword ptr [ebp-4]
23. 00401062 add eax,4
24. 00401065 mov dword ptr [ebp-4],eax
25. 00401068 mov ecx,dword ptr [ebp-4]
26. 0040106B mov edx,dword ptr [ecx-4]
27. 0040106E mov dword ptr [t],edx
if ( t > m )
28. 00401071 mov eax,dword ptr [t]
29. 00401074 cmp eax,dword ptr [ebp-8]
30. 00401077 jle max+5Fh (0040107f)
m = t;
31. 00401079 mov ecx,dword ptr [t]
32. 0040107C mov dword ptr [ebp-8],ecx
}
33. 0040107F jmp max+2Eh (0040104e)
va_end (ap);
34. 00401081 mov dword ptr [ebp-4],0
return m;
35. 00401088 mov eax,dword ptr [ebp-8]
}
36. 0040108B pop edi
37. 0040108C pop esi
38. 0040108D pop ebx
39. 0040108E mov esp,ebp
40. 00401090 pop ebp
41. 00401091 ret
分析上述反匯編代碼,對于一個真正的程序員而言,將是一種很大的享受;而對于初學(xué)者,也將使其受益良多。所以請一定要賴著頭皮認(rèn)真研究,千萬不要被嚇倒!
行1~10進(jìn)行執(zhí)行函數(shù)內(nèi)代碼的準(zhǔn)備工作,保存現(xiàn)場。第2行對堆棧進(jìn)行移動;第3行則意味著max函數(shù)為其內(nèi)部局部變量準(zhǔn)備的堆棧空間為50h字節(jié);第11行表示把變量n的內(nèi)存空間安排在了函數(shù)內(nèi)部局部棧底減8的位置(占用4個字節(jié))。
第12~13行非常關(guān)鍵,對應(yīng)著va_start ( ap, num ),這兩行將第一個可變參數(shù)的地址賦值給了指針ap。另外,從第12行可以看出num的地址為ebp+0Ch;從第13行可以看出ap被分配在函數(shù)內(nèi)部局部棧底減4的位置上(占用4個字節(jié))。
第22~27行最為關(guān)鍵,對應(yīng)著va_arg (ap, int)。其中,22~24行的作用為將ap指向下一可變參數(shù)(可變參數(shù)的地址間隔為4個字節(jié),從add eax,4可以看出);25~27行則取當(dāng)前可變參數(shù)的值賦給變量t。這段反匯編很奇怪,它先移動可變參數(shù)指針,再在賦值指令里面回過頭來取先前的參數(shù)值賦給t(從mov edx,dword ptr [ecx-4]語句可以看出)。Visual C++同學(xué)玩得有意思,不知道碰見同樣的情況Visual Basic等其它同學(xué)怎么玩?
第36~41行恢復(fù)現(xiàn)場和堆棧地址,執(zhí)行函數(shù)返回操作。
痛苦的反匯編之旅差不多結(jié)束了,看了這段反匯編我們總算弄明白了可變參數(shù)的存放位置以及它們被讀取的方式,頓覺全省輕松!
2、特殊的調(diào)用約定
除此之外,我們需要了解C/C++函數(shù)調(diào)用對參數(shù)占用空間的一些特殊約定,因?yàn)樵赺cdecl調(diào)用協(xié)議中,有些變量類型是按照其它變量的尺寸入棧的。
例如,字符型變量將被自動擴(kuò)展為一個字的空間,因?yàn)槿霔2僮麽槍Φ氖且粋€字。
參數(shù)n實(shí)際占用的空間為( ( sizeof(n) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 ) ),這就是第2.1節(jié)_INTSIZEOF(v)宏的來歷!
既然如此,前面給出的va_arg ( list, mode )宏為什么玩這么大的飛機(jī)就很清楚了。這個問題就留個讀者您來分析.
http://wp1314.ycool.com/post.3001515.html
posted on 2010-06-17 23:52 Brandon 閱讀(1470) 評論(0) 編輯 收藏 引用 所屬分類: C++