以前只是知道可變參數(shù)怎么用,但是一直對(duì)它的原理是似懂非懂,現(xiàn)在對(duì)計(jì)算機(jī)有了比較深刻的認(rèn)識(shí)之后,回頭再看,豁然開朗。
要理解可變參數(shù),首先要理解函數(shù)調(diào)用約定, 為什么只有__cdecl的調(diào)用約定支持可變參數(shù),而__stdcall就不支持?
實(shí)際上__cdecl和__stdcall函數(shù)參數(shù)都是從右到左入棧,它們的區(qū)別在于由誰(shuí)來(lái)清棧,__cdecl由外部調(diào)用函數(shù)清棧,而__stdcall由被調(diào)用函數(shù)本身清棧, 顯然對(duì)于可變參數(shù)的函數(shù),函數(shù)本身沒法知道外部函數(shù)調(diào)用它時(shí)傳了多少參數(shù),所以沒法支持被調(diào)用函數(shù)本身清棧(__stdcall), 所以可變參數(shù)只能用__cdecll.
另外還要理解函數(shù)參數(shù)傳遞過程中堆棧是如何生長(zhǎng)和變化的,從堆棧低地址到高地址,依次存儲(chǔ) 被調(diào)用函數(shù)局部變量,上一函數(shù)堆棧楨基址,函數(shù)返回地址,參數(shù)1, 參數(shù)2, 參數(shù)3...,相關(guān)知識(shí)可以參考我的這篇
堆棧楨的生成原理 有了上面的知識(shí),我可以知道函數(shù)調(diào)用時(shí),參數(shù)2的地址就是參數(shù)1的地址加上參數(shù)1的長(zhǎng)度,而參數(shù)3的地址是參數(shù)2的地址加上參數(shù)2的長(zhǎng)度,以此類推。
于是我們可以自己寫可變參數(shù)的函數(shù)了, 代碼如下:
int Sum(
int nCount,

)
{
int nSum = 0;
int* p = &nCount;
for(
int i=0; i<nCount; ++i)
{
cout << *(++p) << endl;
nSum += *p;
}
cout << "Sum:" << nSum << endl << endl;
return nSum;
}
string SumStr(
int nCount,

)
{
string str;
int* p = &nCount;
for(
int i=0; i<nCount; ++i)
{
char* pTemp = (
char*)*(++p);
cout << pTemp << endl;
str += pTemp;
}
cout << "SumStr:" << str << endl;
return str;
}
在我們的測(cè)試函數(shù)中nCount表示后面可變參數(shù)的個(gè)數(shù),
int Sum(int nCount,
)會(huì)打印后面的可變參數(shù)Int值,并且進(jìn)行累加;string SumStr(
int nCount,

)
會(huì)打印后面可變參數(shù)字符串內(nèi)容,并連接所有字符串。
然后用下面代碼進(jìn)行測(cè)試:int main()
{
Sum(3, 10, 20, 30);
SumStr(5, "aa", "bb", "cc", "dd", "ff");
system("pause");
return 0;
}
測(cè)試結(jié)果如下:

可以看到,我們上面的實(shí)現(xiàn)有硬編碼的味道,也有沒有做字節(jié)對(duì)齊,為此系統(tǒng)專門給我們封裝了一些支持可變參數(shù)的宏:
//typedef char * va_list;
//#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
//#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
//#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//#define _crt_va_end(ap) ( ap = (va_list)0 )
//#define va_start _crt_va_start
//#define va_arg _crt_va_arg
//#define va_end _crt_va_end
用系統(tǒng)的這些宏,我們的代碼可以這樣寫了:
//use va_arg, praram is int
int SumNew(
int nCount,

)
{
int nSum = 0;
va_list vl = 0;
va_start(vl, nCount);
for(
int i=0; i<nCount; ++i)
{
int n = va_arg(vl,
int);
cout << n << endl;
nSum += n;
}
va_end(vl);
cout << "SumNew:" << nSum << endl << endl;
return nSum;
}
//use va_arg, praram is char*
string SumStrNew(
int nCount,

)
{
string str;
va_list vl = 0;
va_start(vl, nCount);
for(
int i=0; i<nCount; ++i)
{
char* p = va_arg(vl,
char*);
cout << p << endl;
str += p;
}
cout << "SumStrNew:" << str << endl << endl;
return str;
}
可以看到,其中 va_list實(shí)際上只是一個(gè)參數(shù)指針,va_start根據(jù)你提供的最后一個(gè)固定參數(shù)來(lái)獲取第一個(gè)可變參數(shù)的地址,va_arg將指針指向下一個(gè)可變參數(shù)然后返回當(dāng)前值, va_end只是簡(jiǎn)單的將指針清0.
用下面的代碼進(jìn)行測(cè)試:
int main()
{
Sum(3, 10, 20, 30);
SumStr(5, "aa", "bb", "cc", "dd", "ff");
SumNew(3, 1, 2, 3);
SumStrNew(3, "12", "34", "56");
system("pause");
return 0;
}
結(jié)果如下:

我們上面的例子傳的可變參數(shù)都是4字節(jié)的, 如果我們的可變參數(shù)傳的是一個(gè)結(jié)構(gòu)體,結(jié)果會(huì)怎么樣呢?
下面的例子我們傳的可變參數(shù)是std::string
//use va_arg, praram is std::string
void SumStdString(int nCount,
){ string str; va_list vl = 0; va_start(vl, nCount); for(int i=0; i<nCount; ++i) { string p = va_arg(vl, string); cout << p << endl; str += p; } cout << "SumStdString:" << str << endl << endl;
}
int main()
{
Sum(3, 10, 20, 30);
SumStr(5, "aa", "bb", "cc", "dd", "ff");
SumNew(3, 1, 2, 3);
SumStrNew(3, "12", "34", "56");
string s1("hello ");
string s2("world ");
string s3("!");
SumStdString(3, s1, s2, s3);
system("pause");
return 0;
}
運(yùn)行結(jié)果如下:

可以看到即使傳入的可變參數(shù)是std::string, 依然可以正常工作。
我們可以反匯編下看看這種情況下的參數(shù)傳遞過程:

很多時(shí)候編譯器在傳遞類對(duì)象時(shí),即使是傳值,也會(huì)在堆棧上通過push對(duì)象地址的方式來(lái)傳遞,但是上面顯然沒有這么做,因?yàn)樗獫M足可變參數(shù)堆棧內(nèi)存連續(xù)分布的規(guī)則, 另外,可以看到最后在調(diào)用sumStdString后,由add esp, 58h來(lái)外部清棧。
一個(gè)std::string大小是28, 58h = 88 = 28 + 28 + 28 + 4.
從上面的例子我們可以看到,對(duì)于可變參數(shù)的函數(shù),有2種東西需要確定,一是可變參數(shù)的數(shù)量, 二是可變參數(shù)的類型,上面的例子中,參數(shù)數(shù)量我們是在第一個(gè)參數(shù)指定的,參數(shù)類型我們是自己約定的。這種方式在實(shí)際使用中顯然是不方便,于是我們就有了_vsprintf, 我們根據(jù)一個(gè)格式化字符串的來(lái)表示可變參數(shù)的類型和數(shù)量,比如C教程中入門就要學(xué)習(xí)printf, sprintf等。
總的來(lái)說可變參數(shù)給我們提供了很高的靈活性和方便性,但是也給會(huì)造成不確定性,降低我們程序的安全性,很多時(shí)候可變參數(shù)數(shù)量或類型不匹配,就會(huì)造成一些不容察覺的問題,只有更好的理解它背后的原理,我們才能更好的駕馭它。
posted on 2012-09-18 00:04
Richard Wei 閱讀(1986)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
C++