C語言可變參數函數_初探

一、什麼是可變參數函數

C語言允許定義參數數量可變的函數,這稱爲可變參數函數(variadic function)。這種函數需要固定數量的強制參數,後面是數量可變的可選參數。

其中,強制參數必須至少一個,可選參數數量可變,類型可變,可選參數的數量由強制參數的值決定,或由用來定義可選參數列表的特殊值決定。

其實我們早就接觸過可變參數函數了,C 語言中最常用的可變參數函數例子是 printf()和 scanf()。這兩個函數都有一個強制參數,即格式化字符串。格式化字符串中的轉換修飾符決定了可選參數的數量和類型。(是吧,printf中可以有自定義個%d,沒毛病)

可變參數函數的參數列表的格式是,強制性參數在前,後面跟着一個逗號和省略號(…),這個省略號代表可選參數。比如:int fun(int, …) (我隨便舉的例子啊)
.
.

二、可變參數函數的實現

1、問題引入

我們先來思考這樣一個問題,作爲本節的引入:
如果我們要預先寫一個可變參數的累加求和函數,其強制參數類型爲int,用它來表示我們一共要傳入多少個可變參數。於是我們大概可以有這麼一個框架:

double getSum(int NumofPara, ...)
{
	int i = 0;
	double sum = 0.0;
	
	for( i = 0; i < NumofPara; i++ )
	{
		sum += ??		
	}
}

發現什麼問題沒有?

由於已經知道要傳入多少個可變參數,所以求和思路就是,for循環遍歷NumofPara次,每次都把sum加上一個可變參數。

思路很清晰,沒毛病。但是,問題來了。由於是可變參數,我們無法提前得知可變參量的名字,也就沒法訪問這些可變參數。(你參數列表裏都是一串省略號了,你怎麼可能提前知道變量名,所以自然而然也無法表示、無法訪問這些變量了)

2、實現思路

爲了解決上述問題,C語言規定:

當編寫可變參數函數時,必須用 va_list 類型定義參數指針,以獲取可選參數。可變參數函數要獲取可選參數時,必須通過一個類型爲 va_list 的對象來進行訪問,它包含了參數信息。

這種類型的對象也稱爲參數指針(argument pointer),它包含了棧中至少一個參數的位置。可以使用這個參數指針從一個可選參數移動到下一個可選參數,由此,函數就可以獲取所有的可選參數。va_list 類型被定義在頭文件 stdarg.h 中。

這麼說可能太過官方,太抽象了。我們來舉個例子。

假設我們有一個可變參數函數getSum(int NumofPara, …),然後現在我們代入具體值,比如getSum(3, 7, 8, 9),通過上面的介紹我們知道,第一個3是強制參數,表明後面跟了3個可變參量,而後面的7、8、9則爲具體的可變參量。

根據C語言的要求,我們需要在getSum(int NumofPara, …)函數中定義一個va_list類型的指針。

然後它是怎麼實現“訪問、獲取可選參數”的呢?
圖片講解va_list
C語言裏是這樣實現的:通過某種機制(等會兒會講)讓強制參數和可選參數在內存中以連續的方式存放(強制參數在前),同時讓va_list指針指向最後一個強制參數,即第一個可選參數前的強制參數。然後,通過另一種機制,每次訪問va_list所指的參量之後,指針自動向後移位。進而當下一次再訪問va_list的時候,訪問的就是下一位的值。這樣就可以訪問各個可變參數了。大概就是這樣了,講得太接地氣了。

3、具體實現函數

那麼,C語言又是怎麼實現上面提到的這些機制的呢?

由此引入兩個函數。

void va_start(va_list argptr, lastparam);
是va_list指針的初始化函數,用來初始化指針,也就是實現讓其“先指向最後一個強制參數”的功能。

於是自然而然的,該函數的第一個參數是一個va_list 類型的指針,第二個參數是可變參數函數中最後一個強制參數,即第一個可選參數前的強制參數。

va_start函數中,va_list進行初始化,指針指向末尾的強制參數。va_start結束後,初始化完成,指針自動移位到下一個參數,即第一個可變參數。(雖然感覺有點奇怪)

那麼怎麼訪問va_list指針當前指向的可變參數呢?引出第二個函數:

type va_arg(va_list argptr, type);
其第一個參數是已經初始化完成的va_list指針,第二個參數則爲可變參數的類型,返回的參數就是當前va_list指針所指的可變參數,所以類型也跟傳入的可變參數類型相同。

每一次通過va_arg函數訪問完一次參數後,va_list指針會自動移位到下一位。

C語言還規定,當不再需要使用參數指針時,必須調用宏 va_end來終結該指針,其實說白了就是釋放內存。(如果想使用宏 va_start 或者宏 va_copy 來重新初始化一個之前用過的參數指針,也必須先調用宏 va_end)

4、具體實現示例

好了,說了這麼多,我們通過完善之前寫了一半的getSum函數來具體瞭解一下,可能就會豁然開朗明明白白了:

double getSum(int NumofPara, ...)
{
	int i = 0;							//用於for循環 
	double sum = 0.0;					//用於求和 

	va_list pointer;					//新建一個va_list類型的指針 

	va_start(pointer, NumofPara);		//初始化指針,指針指向確定 
	
	for( i = 0; i < NumofPara; i++ )
	{
		sum += va_arg(pointer, double);	//通過va_arg函數來訪問可變參數,返回類型爲double 
	}									//同時,每次va_arg函數結束後,va_list指針指向下一位 
	
	va_end(pointer);					//終結指針,釋放內存 
	
	return sum;

	//P.S. 有個問題要注意一下,在main函數裏調用的時候,應該寫成getSum(3, 7.0, 8.0, 9.0),否則得到的可能是0。
}

接下來簡單補充一下上面提到的所謂“機制”:

它的實現原理利用了內存的壓棧技術,將參數壓入(push)棧內,使用時,再逐個從棧裏pop出來
需要注意的是,壓棧的順序是從最右邊參數開始的,再向左逐個壓入,根據棧的原理,在取參數時,就從第一個可變參數開始了。
在進程中,堆棧地址是從高到低分配的.當執行一個函數的時候,將參數列表入棧,壓入堆棧的高地址部分,然後入棧函數的返回地址,接着入棧函數的執行代碼,這個入棧過程,堆棧地址不斷遞減。
(所以取的時候就是從低到高,也就是上面草圖中從左到右從3到7到8到9)

三、不小心寫多了一個標題

上面說得比較粗糙,可能有一些地方說錯了。裏面具體原理我也還沒去深究。以後吧。

參考資料:
C語言可變參數函數
【乾貨】C語言可變參數
C語言可變參數列表知識總結
C語言中可變參數的使用方法
基於本文知識點,下一次再來追補一些關於vsprintf函數的知識點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章