淺析C/C++中的可變參數(shù)與默認參數(shù)
千萬要注意,C不支持默認參數(shù)
C/C++支持可變參數(shù)個數(shù)的函數(shù)定義,這一點與C/C++語言函數(shù)參數(shù)調用時入棧順序有關,首先引用其他網(wǎng)友的一段文字,來描述函數(shù)調用,及參數(shù)入棧:
------------ 引用開始 ------------
C支持可變參數(shù)的函數(shù),這里的意思是C支持函數(shù)帶有可變數(shù)量的參數(shù),最常見的例子就是我們十分熟悉的printf()系列函數(shù)。我們還知道在函數(shù)調用時參數(shù)是自右向左壓棧的。如果可變參數(shù)函數(shù)的一般形式是:
f(p1, p2, p3, …)
那么參數(shù)進棧(以及出棧)的順序是:
…
push p3
push p2
push p1
call f
pop p1
pop p2
pop p3
…
我可以得到這樣一個結論:如果支持可變參數(shù)的函數(shù),那么參數(shù)進棧的順序幾乎必然是自右向左的。并且,參數(shù)出棧也不能由函數(shù)自己完成,而應該由調用者完成。
這個結論的后半部分是不難理解的,因為函數(shù)自身不知道調用者傳入了多少參數(shù),但是調用者知道,所以調用者應該負責將所有參數(shù)出棧。
在可變參數(shù)函數(shù)的一般形式中,左邊是已經確定的參數(shù),右邊省略號代表未知參數(shù)部分。對于已經確定的參數(shù),它在棧上的位置也必須是確定的。否則意味著已經確定的參數(shù)是不能定位和找到的,這樣是無法保證函數(shù)正確執(zhí)行的。衡量參數(shù)在棧上的位置,就是離開確切的函數(shù)調用點(call f)有多遠。已經確定的參數(shù),它在棧上的位置,不應該依賴參數(shù)的具體數(shù)量,因為參數(shù)的數(shù)量是未知的!
所以,選擇只能是,已經確定的參數(shù),離開函數(shù)調用點有確定的距離(較近)。滿足這個條件,只有參數(shù)入棧遵從自右向左規(guī)則。也就是說,左邊確定的參數(shù)后入棧,離函數(shù)調用點有確定的距離(最左邊的參數(shù)最后入棧,離函數(shù)調用點最近)。
這樣,當函數(shù)開始執(zhí)行后,它能找到所有已經確定的參數(shù)。根據(jù)函數(shù)自己的邏輯,它負責尋找和解釋后面可變的參數(shù)(在離開調用點較遠的地方),通常這依賴于已經確定的參數(shù)的值(典型的如prinf()函數(shù)的格式解釋,遺憾的是這樣的方式具有脆弱性)。
據(jù)說在pascal中參數(shù)是自左向右壓棧的,與C的相反。對于pascal這種只支持固定參數(shù)函數(shù)的語言,它沒有可變參數(shù)帶來的問題。因此,它選擇哪種參數(shù)進棧方式都是可以的。
甚至,其參數(shù)出棧是由函數(shù)自己完成的,而不是調用者,因為函數(shù)的參數(shù)的類型和數(shù)量是完全已知的。這種方式比采用C的方式的效率更好,因為占用更少的代碼量(在C中,函數(shù)每次調用的地方,都生成了參數(shù)出棧代碼)。
C++為了兼容C,所以仍然支持函數(shù)帶有可變的參數(shù)。但是在C++中更好的選擇常常是函數(shù)重載。
------------ 引用結束 ------------
根據(jù)上文描述,我們查看printf()及sprintf()等函數(shù)的定義,可以驗證這一點:
_CRTIMP int __cdecl printf(const char *, ...);
_CRTIMP int __cdecl sprintf(char *, const char *, ...);
這兩個函數(shù)定義時,都使用了__cdecl關鍵字,__cdecl關鍵字約定函數(shù)調用的規(guī)則是:
調用者負責清除調用堆棧,參數(shù)通過堆棧傳遞,入棧順序是從右到左。
下一步,我們來看看printf()這種函數(shù)是如何使用變個數(shù)參數(shù)的,下面是摘錄MSDN上的例子,
只引用了ANSI系統(tǒng)兼容部分的代碼,UNIX系統(tǒng)的代碼請直接參考MSDN。
------------ 例子代碼 ------------
#include <stdio.h>
#include <stdarg.h>
int average( int first, ... );
void main( void )
{
printf( "Average is: %d/n", average( 2, 3, 4, -1 ) );
}
int average( int first, ... )
{
int count = 0, sum = 0, i = first;
va_list marker;
va_start( marker, first ); /* Initialize variable arguments. */
while( i != -1 )
{
sum += i;
count++;
i = va_arg( marker, int);
}
va_end( marker ); /* Reset variable arguments. */
return( sum ? (sum / count) : 0 );
}
上例代碼功能是計算平均數(shù),函數(shù)允許用戶輸入多個整型參數(shù),要求作后一個參數(shù)必須是-1,表示參數(shù)輸入完畢,然后返回平均數(shù)計算結果。
邏輯很簡單,首先定義
va_list marker;
表示參數(shù)列表,然后調用va_start()初始化參數(shù)列表。注意va_start()調用時不僅使用了marker
這個參數(shù)列表變量,還使用了first這個參數(shù),說明參數(shù)列表的初始化與函數(shù)給定的第一個確定參數(shù)是有關系的,這一點很關鍵,后續(xù)分析會看到原因。
調用va_start()初始化后,即可調用va_arg()函數(shù)訪問每一個參數(shù)列表中的參數(shù)了。注意va_arg()
的第二個參數(shù)指定了返回值的類型(int)。
當程序確定所有參數(shù)訪問結束后,調用va_end()函數(shù)結束參數(shù)列表訪問。
這樣看起來,訪問變個數(shù)參數(shù)是很容易的,也就是使用va_list,va_start(),va_arg(),va_end()
這樣一個類型與三個函數(shù)。但是對于函數(shù)變個數(shù)參數(shù)的機制,感覺仍是一頭霧水。看來需要繼續(xù)深入探究,才能的到確切的答案了。
找到va_list,va_start(),va_arg(),va_end()的定義,在.../VC98/include/stdarg.h文件中。
.h中代碼如下(只摘錄了ANSI兼容部分的代碼,UNIX等其他系統(tǒng)實現(xiàn)略有不同,感興趣的朋友可以自己研究):
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 )
從代碼可以看出,va_list只是一個類型轉義,其實就是定義成char*類型的指針了,這樣就是為了以字節(jié)為單位訪問內存。
其他三個函數(shù)其實只是三個宏定義,且慢,我們先看夾在中間的這個宏定義_INTSIZEOF:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
這個宏的功能是對給定變量或者類型n,計算其按整型字節(jié)長度進行字節(jié)對齊后的長度(size)。在32位系統(tǒng)中int占4個字節(jié),16位系統(tǒng)中占2字節(jié)。
表達式
(sizeof(n) + sizeof(int) - 1)
的作用是,如果sizeof(n)小于sizeof(int),則計算后
的結果數(shù)值,會比sizeof(n)的值在二進制上向左進一位。
如:sizeof(short) + sizeof(n) - 1 = 5
5的二進制是0x00000101,sizeof(short)的二進制是0x00000010,所以5的二進制值比2的二進制值
向左高一位。
表達式
~(sizeof(int) - 1)
的作用時生成一個蒙版(mask),以便舍去前面那個計算值的"零頭"部分。
如上例,~(sizeof(int) - 1) = 0x00000011(謝謝glietboys的提醒,此處應該是0xFFFFFF00)
同5的二進制0x00000101做"與"運算得到的是0x00000100,也就是4,而直接計算sizeof(short)應該得到2。
這樣通過_INTSIZEOF(short)這樣的表達式,就可以得到按照整型字節(jié)長度對齊的其他類型字節(jié)長度。
之所以采用int類型的字節(jié)長度進行對齊,是因為C/C++中的指針變量其實就是整型數(shù)值,長度與int相同,而指針的偏移量是后面的三個宏進行運算時所需要的。
關于編程中字節(jié)對齊的內容請有興趣的朋友到網(wǎng)上參考其他文章,這里不再贅述。
繼續(xù),下面這個三個宏定義:
第一:
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
編程中這樣使用
va_list marker;
va_start( marker, first );
可以看出va_start宏的作用是使給定的參數(shù)列表指針(marker),根據(jù)第一個確定參數(shù)(first)所屬類型的指針長度向后偏移相應位置,計算這個偏移的時候就用到了前面的_INTSIZEOF(n)宏。
第二:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
此處乍一看有點費解,(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)表達式的一加一減,對返回值是不起作用的啊,也就是返回值都是ap的值,什么原因呢?
原來這個計算返回值是一方面,另一方面,請記住,va_start(),va_arg(),va_end這三個宏的調用是有關聯(lián)性的,ap這個變量是調用va_start()時給定的參數(shù)列表指針,所以
(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)
表達式不僅僅是為了返回當前指向的參數(shù)的地址,還是為了讓ap指向下一個參數(shù)(注意ap跳向下一參數(shù)是,是按照類型t的_INTSIZEOF長度進行計算的)。
第三:
#define va_end(ap) ( ap = (va_list)0 )
這個很好理解了,不過是將ap指針置為空,算作參數(shù)讀取結束。
至此,C/C++變個數(shù)函數(shù)參數(shù)的機制已經很清晰了。最后還要說一點要注意的問題:
在用va_arg()順序跳轉指針讀取參數(shù)的過程中,并沒有方法去判斷所得到的下一個指針是否是有效地址,也沒有地方能夠明確得知到底要讀取多少個參數(shù),這就是這種變個數(shù)參數(shù)的危險所在。前面的求平均數(shù)的例子中,要求輸入者必須在參數(shù)列表最后提供一個特殊值(-1)來表示參數(shù)列表結束,所以可以假設,萬一調用者沒有遵循這種規(guī)則,將導致指針訪問越界。
那么,可能有朋友會問,printf()函數(shù)就沒有提供這樣的特殊值進行標識啊。
別急,printf()使用的是另一種參數(shù)個數(shù)識別方式,可能比較隱蔽。注意他的第一個確定參數(shù),也就是被我們用作格式控制的format字符串,他的里面有"%d","%s"這樣的參數(shù)描述符,printf()函數(shù)在解析format字符串時,可以根據(jù)參數(shù)描述符的個數(shù),確定需要讀取后面幾個參數(shù)。我們不妨做下面這樣的試驗:
printf("%d,%d,%d,%d/n",1,2,3,4,5);
實際提供的參數(shù)多于前面給定的參數(shù)描述符,這樣執(zhí)行的結果是
1,2,3,4
也就是printf()根據(jù)format字符串認為后面只有4個參數(shù),其他的就不管了。那么再做一個試驗:
printf("%d,%d,%d,%d/n",1,2,3);
實際提供的參數(shù)少于給定的參數(shù)描述符,這樣執(zhí)行的結果是(如果沒有異常的話)
1,2,3,2367460
這個地方,每個人的執(zhí)行結果可能都不相同,原因是讀取最后一個參數(shù)的指針已經指向了非法的地址。這也是使用printf()這類函數(shù)需要特別注意的地方。
總結:
變個數(shù)的函數(shù)參數(shù)在使用時需要注意的地方比較多。我個人建議盡量回避使用這種模式。比如前面的計算平均數(shù),寧可使用數(shù)組或其他列表作為參數(shù)將一系列數(shù)值傳遞給函數(shù),也不用寫這樣的變態(tài)函數(shù)。一方面是容易出現(xiàn)指針訪問越界,另一方面,在實際的函數(shù)調用時,要把所有計算值依次作為參數(shù)寫在代碼里,很齷齪。
雖然這么說,但有些地方這個功能還是很有用處的,比如字符串的格式化合成,像printf()函數(shù);在實際應用中,我還經常使用一個自己寫的WriteLog()函數(shù),用于記錄文件日志,定義與printf()相同,使用起來非常靈活便利,如:
WriteLog("用戶%s, 登錄次數(shù)%d","guanzhong",10);
寫在文件里的內容就是
用戶guanzhong, 登錄次數(shù)10
編程語言的使用,在遵循基本規(guī)則的前提下,是仁者見仁,智者見智??傊笍亓私庵?,選擇一個符合自己的好的習慣即可
上一篇:枚舉和宏的區(qū)別詳細解析
欄 目:C語言
下一篇:STl中的排序算法詳細解析
本文標題:淺析C/C++中的可變參數(shù)與默認參數(shù)
本文地址:http://mengdiqiu.com.cn/a1/Cyuyan/4093.html
您可能感興趣的文章
- 04-02c語言沒有round函數(shù) round c語言
- 01-10深入理解C++中常見的關鍵字含義
- 01-10使用C++實現(xiàn)全排列算法的方法詳解
- 01-10深入Main函數(shù)中的參數(shù)argc,argv的使用詳解
- 01-10c++中inline的用法分析
- 01-10如何尋找數(shù)組中的第二大數(shù)
- 01-10用C++實現(xiàn)DBSCAN聚類算法
- 01-10全排列算法的非遞歸實現(xiàn)與遞歸實現(xiàn)的方法(C++)
- 01-10C++大數(shù)模板(推薦)
- 01-10淺談C/C++中的static與extern關鍵字的使用詳解


閱讀排行
本欄相關
- 04-02c語言函數(shù)調用后清空內存 c語言調用
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言的正則匹配函數(shù) c語言正則表達
- 04-02c語言用函數(shù)寫分段 用c語言表示分段
- 04-02c語言中對數(shù)函數(shù)的表達式 c語言中對
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段
- 04-02C語言中怎么打出三角函數(shù) c語言中怎
- 04-02c語言調用函數(shù)求fibo C語言調用函數(shù)求
隨機閱讀
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 01-10delphi制作wav文件的方法
- 01-11ajax實現(xiàn)頁面的局部加載
- 04-02jquery與jsp,用jquery
- 01-10SublimeText編譯C開發(fā)環(huán)境設置
- 01-10使用C語言求解撲克牌的順子及n個骰子
- 01-10C#中split用法實例總結