C函式引數中的三個點
C++中有函式過載這種方法,以供我們呼叫時要可以不確定實參的個數,其實 C 語言也可以,而且更高明!
我們在stdio.h 中可以看到 printf() 函式的原型:
int printf(char * format,...)
事實上,我們如果要寫這樣的函式也可以類似的寫,那麼在定義函式時用上這個符號“ ... ” ,它叫佔位符,喊它 “ 三個點 ” 也可以,只要你願意!那麼我可以這樣定義我的函式:
fun(int a,...)
{ }
只要上課認真聽了的同學(傻瓜除外)都知道,這是個空函式,它是什麼都不做的,光這樣寫還不行的,具體應該怎樣定義呢?
且聽我介紹3
個小東東:
1、 va_list
2、 va_arg()
3、 va_start()
在學習這3 個小東東之前,我們先回憶一下, C 語言是怎麼操作檔案時,是怎麼樣處理記憶體中的資料的呢?學習檔案操作時,我們提到了“流”的概念,我們用指標指向資料所在的記憶體地址,再一個一個的操作。
學習指標時,我們知道有函式指標這個東東,不是指標函式而是函式打針哦!(呵呵,我的同學如果還記得就當複習一下,不要嫌我囉嗦^_^
)。我們記得程式在執行時,會將函式儲存到記憶體中去。現在深入的講一點點,儲存函式時,引數傳遞的過程是怎樣實現的呢?所謂的形式引數(區域性變數)實質上又是什麼呢?把這些問題連起來想想,想通了,你的思維勢如破竹!
在呼叫函式時,程式同樣會把實參傳入,在函式儲存區儲存起來,如果有很多引數,將一起儲存起來。
這時候就要用到va_list 了,這是個型別定義,我們可以把它理解成一個指標,它指向第一個引數的地址。
如果,我們這樣定義: va_list pp ;
則pp 就是這樣一種變數,它是指向所有引數中的第一個引數的。它不同於一般的指標變數,它是個複合變數,什麼是複合變數啊?結構體型別的嘛,呵呵。如果 a 是第一個引數,能不能寫成 pp=a 呢?
假設我定義了char d[]="ruixin",e[]="gelin";
我要把
e
的值賦給
d
,能不能寫成
d=e
呢?得用
strcpy()
如果這樣寫:va_start(pp,a);
那麼pp 就指向第一個引數 a 了,並且可得到 a 的型別 int 。
這時候如果有下一個引數,就需要使pp 指向下一個引數,並且得到它的型別。同樣需要使用函式來實現,這個函式是: va_arg()
可以這樣寫:va_arg(pp, 型別 ) ,這樣 pp 就指向一個引數,並且可以得到那個引數的型別了。
注意!型別非常重要,學過指標的都應該清楚,指標的型別如果弄錯的話,位置正確,取出來的數可能也是亂七八糟的。
下面我們看一個簡單的例子:
#include <stdio.h>
#include<stdarg.h>
void fun(int a,...)
{
va_list pp;
int n=1;//使用 n 計量引數個數
va_start(pp,a);
do
{
printf("第 %d 個引數 =%d/n",n++,s);
a=va_arg(pp,int);//使 pp 指向下一個引數,將下一個引數的值賦給變數 a
}
while (a!=0);//直到引數為 0 時停止迴圈
}
main()
{
fun(20,40,60,80,0);
}
注意!
一定要有上面兩個檔案包含命令,因為程式中用到的那3個小東東都在那個檔案裡。其實真正意義上應該說那是函式,實質上那不過是兩個巨集,呵呵。
什麼是巨集,什麼是函式,不是這兒要講的,也和這沒太大關係。寫出來,僅僅是回答一個為什麼……
VA_LIST 是在C語言中解決變參問題的一組巨集
VA_LIST的用法:
(1)首先在函式裡定義一具VA_LIST型的變數,這個變數是指向引數的指標
(2)然後用VA_START巨集初始化變數剛定義的VA_LIST變數,這個巨集的第二個引數是第一個可變引數的前一個引數,是一個固定的引數。
(3)然後用VA_ARG返回可變的引數,VA_ARG的第二個引數是你要返回的引數的型別。
(4)最後用VA_END巨集結束可變引數的獲取。然後你就可以在函式裡使用第二個引數了。如果函式有多個可變引數的,依次呼叫VA_ARG獲取各個引數。
VA_LIST在編譯器中的處理:
1)在執行VA_START(ap,v)以後,ap指向第一個可變引數在堆疊的地址。
(2)VA_ARG()取得型別t的可變引數值,在這步操作中首先apt = sizeof(t型別),讓ap指向下一個引數的地址。然後返回ap-sizeof(t型別)的t型別*指標,這正是第一個可變引數在堆疊裡的地址。然後用*取得這個地址的內容。
(3)VA_END(),X86平臺定義為ap = ((char*)0),使ap不再指向堆疊,而是跟NULL一樣,有些直接定義為((void*)0),這樣編譯器不會為VA_END產生程式碼,例如gcc在Linux的X86平臺就是這樣定義的。
要注意的是:由於引數的地址用於VA_START巨集,所以引數不能宣告為暫存器變數,或作為函式或陣列型別。
使用VA_LIST應該注意的問題:
(1)因為va_start, va_arg,
va_end等定義成巨集,所以它顯得很愚蠢,可變引數的型別和個數完全在該函式中由程式程式碼控制,它並不能智慧地識別不同引數的個數和型別.
也就是說,你想實現智慧識別可變引數的話是要通過在自己的程式裡作判斷來實現的.
(2)另外有一個問題,因為編譯器對可變引數的函式的原型檢查不夠嚴格,對程式設計查錯不利.不利於我們寫出高質量的程式碼。
小結:可變引數的函式原理其實很簡單,而
VA系列是以巨集定義來定義的,實現跟堆疊相關。我們寫一個可變函式的C函式時,有利也有弊,所以在不必要的
場合,我們無需用到可變引數,如果在C++裡,我們應該利用C++多型性來實現可變引數的功能,儘量避免用C語言的方式來實現。