java 变长参数 知乎_变长参数探究
前言
變長參數(shù),指的是函數(shù)參數(shù)數(shù)量可變,或者說函數(shù)接受參數(shù)的數(shù)量可以不固定。實(shí)際上,我們最開始學(xué)C語言的時(shí)候,就用到了這樣的函數(shù):printf,它接受任意數(shù)量的參數(shù),向終端格式化輸出字符串。本文就來探究一下,變長參數(shù)函數(shù)的實(shí)現(xiàn)機(jī)制是怎樣的,以及我們自己如何實(shí)現(xiàn)一個(gè)變長參數(shù)函數(shù)。在此之前,我們先來了解一下參數(shù)入棧順序是怎樣的。
函數(shù)參數(shù)入棧順序
我們可能知道,參數(shù)入棧順序是從右至左,是不是這樣的呢?我們可以通過一個(gè)小程序驗(yàn)證一下。小程序做的事情很簡單,main函數(shù)調(diào)用了傳入8個(gè)參數(shù)的test函數(shù),test函數(shù)打印每個(gè)參數(shù)的地址。
#include
void test(int a,int b,int c,int d,int e,int f,int g,int h)
{
printf("%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n",&a,&b,&c,&d,&e,&f,&g,&h);
}
int main(int argc,char *argv[])
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
int f = 6;
int g = 7;
int h = 8;
test(a,b,c,d,e,f,g,h);
return 0;
}
編譯成32位程序:
gcc -m32 -o paraTest paraTest.c
運(yùn)行(不同的機(jī)器運(yùn)行結(jié)果不同,且每次運(yùn)行結(jié)果也不一定相同):
0xffdadff0
0xffdadff4
0xffdadff8
0xffdadffc
0xffdae000
0xffdae004
0xffdae008
0xffdae00c
觀察打印出來的地址,可以發(fā)現(xiàn),從a到h地址值依次增加4。我們知道,棧是從高地址向低地址增長的,從地址值可以推測h是最先入棧,a是最后入棧的。也就是說,參數(shù)是從右往左入棧的(注:并非所有語言都是如此)。
但是如果將函數(shù)test參數(shù)b改為char 型呢?運(yùn)行結(jié)果如下:
0xffb29500
0xffb294ec
0xffb29508
0xffb2950c
0xffb29510
0xffb29514
0xffb29518
0xffb2951c
觀察結(jié)果可以發(fā)現(xiàn),b的地址并非是a的地址值加4,也不是在a和c的地址值之間,這是為何?這是編譯器出于對空間,壓棧速度等因素的考慮,對其進(jìn)行了優(yōu)化,但這并不影響變長參數(shù)的實(shí)現(xiàn)。
對于上面的情況,如果我們編譯成64位程序又是什么樣的情況呢?
gcc -o paraTest paraTest.c
./paraTest
運(yùn)行結(jié)果如下:
0x7fff4b83cbcc
0x7fff4b83cbc8
0x7fff4b83cbc4
0x7fff4b83cbc0
0x7fff4b83cbbc
0x7fff4b83cbb8
0x7fff4b83cbe0
0x7fff4b83cbe8
通過觀察可以發(fā)現(xiàn),從參數(shù)a到f,其地址似乎是遞減的,而從g到h地址又變成遞增的了,這是為什么呢?事實(shí)上,對于x86-64,當(dāng)參數(shù)個(gè)數(shù)超過6時(shí),前6個(gè)參數(shù)可以通過寄存器傳遞,而第7~n個(gè)參數(shù)則會通過棧傳遞,并且數(shù)據(jù)大小都向8的倍數(shù)對齊。也就是說,對于7~n個(gè)參數(shù),依然滿足從右往左入棧,只是對于前6個(gè)參數(shù),它們是通過寄存器來傳遞的。另外,寄存器的訪問速度相對于內(nèi)存來說要快得多,因此為了提高空間和時(shí)間效率,實(shí)際中其實(shí)不建議參數(shù)超過6個(gè)。
對于函數(shù)參數(shù)入棧順序我們就了解到這里,但是參數(shù)入棧順序和變長參數(shù)又有什么關(guān)系呢?
變長參數(shù)實(shí)現(xiàn)分析
通過前面的例子,我們了解到函數(shù)參數(shù)是從右往左依次入棧的,而且第一個(gè)參數(shù)位于棧頂。那么,我們就可以通過第一個(gè)參數(shù)進(jìn)行地址偏移,來得到第二個(gè),第三個(gè)參數(shù)的地址,是不是可以實(shí)現(xiàn)呢?我們來看一個(gè)32位程序的例子。例子同樣很簡單,我們通過a的地址來獲取其他參數(shù)的地址:
#include
void test( int a, char b, int c, int d, int e)
{
printf("%d\n%d\n%d\n%d\n%d\n\n",a,*(&a+1),*(&a+2),*(&a+3),*(&a+4));
}
int main(int argc,char *argv[])
{
int a = 1;
char b = 2;
int c = 3;
int d = 4;
int e = 5;
test(a,b,c,d,e);
return 0;
}
編譯為32位程序運(yùn)行:
gcc -m32 -o paraTest paraTest.c
./paraTest
1
2
3
4
5
通過觀察運(yùn)行結(jié)果我們可以發(fā)現(xiàn),即使只有a的地址也可以訪問到其他參數(shù)。也就是說,即便傳入的參數(shù)是多個(gè),只要我們知道每個(gè)參數(shù)的類型,只需通過第一個(gè)參數(shù)就能夠通過地址偏移正確訪問到其他參數(shù)。同時(shí)我們也注意到,即便b是char類型,訪問c的值也是偏移4的倍數(shù)地址,這是字節(jié)對齊的緣故,有興趣的可以閱讀理一理字節(jié)對齊的那些事。
變長參數(shù)實(shí)現(xiàn)
經(jīng)過前面的理解分析,我們知道,正是由于參數(shù)從右往左入棧(但是要注意的是,對于x86-64,它的參數(shù)不是完全從右往左入棧,且參數(shù)可能不在一個(gè)連續(xù)的區(qū)域中,它的變長參數(shù)實(shí)現(xiàn)也更為復(fù)雜,我們這里不展開)可以實(shí)現(xiàn)變長參數(shù)。當(dāng)然了,這一切,C已經(jīng)有現(xiàn)成可用的一些東西來幫我們實(shí)現(xiàn)變長參數(shù)。
它主要通過一個(gè)類型(va_list)和三個(gè)宏(va_start、va_arg、va_end)來實(shí)現(xiàn)
va_list :存儲參數(shù)的類型信息,32位和64位實(shí)現(xiàn)不一樣。
void va_start ( va_list ap, paramN );
參數(shù):
ap: 可變參數(shù)列表地址
paramN: 確定的參數(shù)
功能:初始化可變參數(shù)列表,會把paraN之后的參數(shù)放入ap中
type va_arg ( va_list ap, type );
功能:返回下一個(gè)參數(shù)的值。
void va_end ( va_list ap );
功能:完成清理工作。
可變參數(shù)函數(shù)實(shí)現(xiàn)的步驟如下:1.在函數(shù)中創(chuàng)建一個(gè)va_list類型變量
2.使用va_start對其進(jìn)行初始化
3.使用va_arg訪問參數(shù)值
4.使用va_end完成清理工作
接下來我們來實(shí)現(xiàn)一個(gè)變長參數(shù)函數(shù)來對給定的一組整數(shù)進(jìn)行求和。程序清單如下:
#include
/*要使用變長參數(shù)的宏,需要包含下面的頭文件*/
#include
/*
* getSum:用于計(jì)算一組整數(shù)的和
* num:整數(shù)的數(shù)量
*
* */
int getSum(int num,...)
{
va_list ap;//定義參數(shù)列表變量
int sum = 0;
int loop = 0;
va_start(ap,num);
/*遍歷參數(shù)值*/
for(;loop < num ; loop++)
{
/*取出并加上下一個(gè)參數(shù)值*/
sum += va_arg(ap,int);
}
va_end(ap);
return sum;
}
int main(int argc,char *argv[])
{
int sum = 0;
sum = getSum(5,1,2,3,4,5);
printf("%d\n",sum);
return 0;
}
上面的小程序接受變長參數(shù),第一個(gè)參數(shù)表明將要計(jì)算和的整數(shù)個(gè)數(shù),后面的參數(shù)是要計(jì)算的值。
編譯運(yùn)行可得結(jié)果:15。
但是我們要注意的是,這個(gè)小程序不像printf那樣,對傳入的參數(shù)做了校驗(yàn),因此一但傳入的參數(shù)num和實(shí)際參數(shù)不匹配,或者傳入類型與要計(jì)算的int類型不匹配,將會出現(xiàn)不可預(yù)知的錯(cuò)誤。我們舉一個(gè)簡單的例子,如果第二個(gè)參數(shù)傳入一個(gè)浮點(diǎn)數(shù),程序清單如下:
#include
/*要使用變長參數(shù)的宏,需要包含下面的頭文件*/
#include
/*
* getSum:用于計(jì)算一組整數(shù)的和
* num:整數(shù)的數(shù)量
*
* */
int getSum(int num,...)
{
va_list ap;//定義參數(shù)列表變量
int sum = 0;
int loop = 0;
int value = 0;
va_start(ap,num);
for(;loop < num ; loop++)
{
value = va_arg(ap,int);
printf("the %d value is %d\n",loop.value);
sum += value;
}
va_end(ap);
return sum;
}
int main(int argc,char *argv[])
{
int sum = 0;
float a = 8.25f;
printf("a to int=%d\n",*(int*)&a);
sum = getSum(5,a,2,3,4,5);
printf("%d\n",sum);
return 0;
}
編譯運(yùn)行:
gcc -m32 -o multiPara multiPara.c
./multiPara
a to int=1090781184
the 0 loop value is 0
the 1 loop value is 1075871744
the 2 loop value is 2
the 3 loop value is 3
the 4 loop value is 4
the sum is1075871753
觀察上面的運(yùn)行結(jié)果,發(fā)現(xiàn)結(jié)果與我們所預(yù)期大相徑庭,我們可能會有以下幾個(gè)疑問:1.把a(bǔ)的地址上的值轉(zhuǎn)換為int,為什么會是1090781184?
2.getSum函數(shù)中,為什么第一個(gè)值是0?
3.getSum函數(shù)中,為什么第二個(gè)值是1075871744?
4.getSum函數(shù)中,為什么沒有獲取到5?
5.為什么最后的結(jié)果不是我們預(yù)期的值?
我們逐一解答第一個(gè)問題,我們不在本文解釋,但可以通過對浮點(diǎn)數(shù)的一些理解來找到答案。
對于第二個(gè)、第三個(gè)問題以及第四個(gè)問題,涉及到類型提升。也就是說在C語言中,調(diào)用一個(gè)不帶原型聲明的函數(shù)時(shí),調(diào)用者會對每個(gè)參數(shù)執(zhí)行“默認(rèn)實(shí)際參數(shù)提升",提升規(guī)則如下:
——float將提升到double
——char、short和相應(yīng)的signed、unsigned類型將提升到int
——如果int不能存儲原值,則提升到unsigned int
那么也就可以理解了,調(diào)用者會將提升之后的參數(shù)傳給被調(diào)用者。也就是說a被提升為了8字節(jié)的double類型,自然而然,而我們?nèi)≈凳前磇nt4字節(jié)取值,第一次取值取的double的前4字節(jié),第二次取的后4字節(jié),而由于總共取數(shù)5次,因此最后的5也就不會被取到。
了解了前面幾個(gè)問題的答案,那么最后一個(gè)問題的答案也就隨之而出了。前面取值已經(jīng)不對了,最后的結(jié)果自然不是我們想要的。
總結(jié)
通過前面的分析和示例,我們來做一些總結(jié)變長參數(shù)實(shí)現(xiàn)的基本原理
對于x86來說,函數(shù)參數(shù)入棧順序?yàn)閺挠彝?#xff0c;因此,在知道第一個(gè)參數(shù)地址之后,我們能夠通過地址偏移獲取其他參數(shù),雖然x86-64在實(shí)現(xiàn)上略有不同,但`對于開發(fā)者使用來說,實(shí)現(xiàn)變長參數(shù)函數(shù)沒有32位和64位的區(qū)別。
變長參數(shù)實(shí)現(xiàn)注意事項(xiàng)
1.…前的參數(shù)可以有1個(gè)或多個(gè),但前一個(gè)必須是確定類型。
2.傳入?yún)?shù)會可能會出現(xiàn)類型提升。
3.va_arg的type類型不能是char,short int,float等類型,否則取值不正確,原因?yàn)榈?#xff12;點(diǎn)。
4.va_arg不能往回取參數(shù),但可以使用va_copy拷貝va_list,以備后用。
5.變長參數(shù)類型注意做好檢查,例如可以采用printf的占位符方式等等。
6.即便printf有類型檢查,但也要注意參數(shù)匹配,例如,將int類型匹配%s打印,將會出現(xiàn)嚴(yán)重問題。
7.當(dāng)傳入?yún)?shù)個(gè)數(shù)少于使用的個(gè)數(shù)時(shí),可能會出現(xiàn)嚴(yán)重問題,當(dāng)傳入?yún)?shù)大于使用的個(gè)數(shù)時(shí),多出的參數(shù)不會被處理使用。
8.注意字節(jié)對齊問題。
總結(jié)
以上是生活随笔為你收集整理的java 变长参数 知乎_变长参数探究的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos7重启桌面服务_CENTOS
- 下一篇: 接受java的返回值_java怎样接受