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