標准庫提供的一些參數的數目可以有變化的函數。例如我們很熟悉的printf,它需要有一個格式串,還應根據需要為它提供任意多個“其他參數”。這種函數被稱作“具有變長度參數表的函數”,或簡稱為“變參數函數”。我們寫程序中有時也可能需要定義這種函數。要定義這類函數,就必須使用標准頭文件<stdarg.h>,使用該文件提供的一套機制,並需要按照規定的定義方式工作。本節介紹這個頭文件提供的有關功能,它們的意義和使用,並用例子說明這類函數的定義方法。
一個變參數函數至少需要有一個普通參數,其普通參數可以具有任何類型。在函數定義中,這種函數的最后一個普通參數除了一般的用途之外,還有其他特殊用途。下面從一個例子開始說明有關的問題。
假設我們想定義一個函數sum,它可以用任意多個整數類型的表達式作為參數進行調用,希望sum能求出這些參數的和。這時我們應該將sum定義為一個只有一個普通參數,並具有變長度參數表的函數,這個函數的頭部應該是(函數原型與此類似):
int sum(int n, ...)
我們實際上要求在函數調用時,從第一個參數n得到被求和的表達式個數,從其余參數得到被求和的表達式。在參數表最后連續寫三個圓點符號,說明這個函數具有可變數目的參數。凡參數表具有這種形式(最后寫三個圓點),就表示定義的是一個變參數函數。注意,這樣的三個圓點只能放在參數表最后,在所有普通參數之后。
為了能在變參數函數里取得並處理不定個數的“其他參數”,頭文件<stdarg.h>提供了一套機制。這里提供了一個特殊類型va_list。在每個變參數函數的函數體里必須定義一個va_list類型的局部變量,它將成為訪問由三個圓點所代表的實際參數的媒介。下面假設函數sum里所用的va_list類型的變量的名字是vap。在能夠用vap訪問實際參數之前,必須首先用“函數”va_start做這個變量初始化。函數va_start的類型特征可以大致描述為:
va_start(va_list vap, 最后一個普通參數)
實際上va_start通常並不是函數,而是用宏定義實現的一種功能。在函數sum里對vap初始化的語句應當寫為:
va_start(vap, n);
在完成這個初始化之后,我們就可以通過另一個宏va_arg訪問函數調用的各個實際參數了。宏va_arg的類型特征可以大致地描述為:
類型 va_arg(va_list vap, 類型名)
在調用宏va_arg時必須提供有關實參的實際類型,這一類型也將成為這個宏調用的返回值類型。對va_arg的調用不僅返回了一個實際參數的值(“當前”實際參數的值),同時還完成了某種更新操作,使對這個宏va_arg的下次調用能得到下一個實際參數。對於我們的例子,其中對宏va_arg的一次調用應當寫為:
v = va_arg(vap, int);
這里假定v是一個有定義的int類型變量。
在變參數函數的定義里,函數退出之前必須做一次結束動作。這個動作通過對局部的va_list變量調用宏va_end完成。這個宏的類型特征大致是:
void va_end(va_list vap);
下面是函數sum的完整定義,從中可以看到各有關部分的寫法:
int sum(int n, ...) {
va_list vap;
int i, s = 0;
va_start(vap, n);
for (i = 0; i < n; i++) s += va_arg(vap, int);
va_end(vap);
return s;
}
這里首先定義了va_list變量vap,而后對它初始化。循環中通過va_arg取得順序的各個實參的值,並將它們加入總和。最后調用va_end結束。
下面是調用這個函數的幾個例子:
k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);
在編寫和使用具有可變數目參數的函數時,有幾個問題值得注意。首先,雖然在上面描述了頭文件所提供的幾個宏的“類型特征”,實際上這僅僅是為了說明問題。因為實際上我們沒辦法寫出來有關的類型,系統在預處理時進行宏展開,編譯時即使發現錯誤,也無法提供關於這些宏調用的錯誤信息。所以,在使用這些宏的時候必須特別注意類型的正確性,系統通常無法自動識別和處理其中的類型轉換問題。
第二:調用va_arg將更新被操作的va_list變量(如在上例的vap),使下次調用可以得到下一個參數。在執行這個操作時,va_arg並不知道實際有幾個參數,也不知道參數的實際類型,它只是按給定的類型完成工作。因此,寫程序的人應在變參數函數的定義里注意控制對實際參數的處理過程。上例通過參數n提供了參數個數的信息,就是為了控制循環。標准庫函數printf根據格式串中的轉換描述的數目確定實際參數的個數。如果這方面信息有誤,函數執行中就可能出現嚴重問題。編譯程序無法檢查這里的數據一致性問題,需要寫程序的人自己負責。在前面章節里,我們一直強調對printf等函數調用時,要注意格式串與其他參數個數之間一致性,其原因就在這里。
第三:編譯系統無法對變參數函數中由三個圓點代表的那些實際參數做類型檢查,因為函數的頭部沒有給出這些參數的類型信息。因此編譯處理中既不會生成必要的類型轉換,也不會提供類型錯誤信息。考慮標准庫函數printf,在調用這個函數時,不但實際參數個數可能變化,各參數的類型也可能不同,因此不可能有統一方式來描述它們的類型。對於這種參數,C語言的處理方式就是不做類型檢查,要求寫程序的人保證函數調用的正確性。
假設我們寫出下面的函數調用:
k = sum(6, 2.4, 4, 5.72, 6, 2);
編譯程序不會發現這里參數類型不對,需要做類型轉換,所有實參都將直接傳給函數。函數里也會按照內部定義的方式把參數都當作整數使用。編譯程序也不會發現參數個數與6不符。這一調用的結果完全由編譯程序和執行環境決定,得到的結果肯定不會是正確的。