如果一定要說哪段C語言代碼最「著名」,我想非「hello world」莫屬了。大多數初學者人生中編寫的第一段C語言代碼就是這段「裡程碑」式的代碼:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;}
也正因為這段著名的程序,printf() 函數成為大多數C語言初學者接觸到的第一個標準庫函數。
C語言中的可變參數函數
隨著學習的推進,初學者逐步學會調用別的C語言函數,以及定義自己的函數,觀察力敏銳的會注意到 printf() 函數似乎與其他函數不太一樣——printf()函數沒有固定數目的參數,它似乎可以接收任意多的參數。
而其他C語言函數則不同,它們大都有固定數量的參數(0個,3個等),調用這些函數必須傳遞對應數目的參數。
有些持有「特殊論」的初學者認為像 printf() 這樣的「可變參數」函數是「特殊的」,是系統定義的,我們程式設計師只能定義固定參數的函數,其實不是的,C語言是有手段定義自己「可變參數」函數的。
printf() 究竟是不是只能由系統定義的「特殊」函數呢?
怎樣定義自己的可變參數函數?
事實上,標準庫 <stdarg.h>就是方便C語言程式設計師定義自己的「可變參數」函數的。如果讀者和我一樣使用的是 Linux 系統,則可以方便的通過 man 命令查詢到相關庫函數:
頭文件<stdarg.h>聲明了 va_list 類型用於描述可變參數,並且定義了上述 4 個方法解析。這裡不打算介紹過多枯燥的理論知識,我們直接看實例,請看相關C語言代碼:
上述代碼定義了可變參數函數 foo(),它可以接收類似於 printf() 的函數,並且將 fmt 中的 s 解析為字符串,d 解析為整數,c 解析為字符,因此編譯並執行這段C語言代碼,可得到如下輸出:
# gcc t.c
# ./a.out
string hello
int 12
char m通過這段實例,可以看出使用C語言定義可變參數函數並不複雜,在處理可變參數時,只需先調用 va_start() 將參數序列加載到 va_list 結構的變量中,然後調用 va_arg() 依次解析。解析完畢後,再調用 va_end() 結束解析。
va_start -> va_arg -> va_end。
唯一需要注意的是使用 va_arg() 解析參數時,需要指定類型。但是這個過程也很簡單,可變參數函數的實現者可以指定一套規則,用於約束函數調用者傳遞參數,這樣就知道接下來需要解析的參數是何種類型。例如上面的C語言代碼就約定了 fmt 中的 s 表示接下來的要解析的參數是字符串,d 表示整數等。
計算機是如何處理可變參數函數的?
C語言定義可變參數函數的過程並不複雜,藉助於<stdarg.h>,我們能夠輕易的定義接收任意多參數的函數,不過到這裡,有讀者發現問題了:我們人類可以按照規則寫出可變參數函數,但是計算機是如何理解這一套規則的呢?或者換句話說,計算機是如何處理「可變參數」的?
以Linux為例,看過我之前文章的讀者應該明白,每個C語言程序進程都有屬於自己的棧,進程中的每個函數則有屬於自己的棧幀,當有函數調用時,例如:
foo("%d%d%d", 3,2,1);
C語言編譯器會產生類似於下面這樣的彙編代碼:
push 1push 2push 3push "%d%d%d"call foo也即將 foo() 函數的參數先壓入棧中,然後再調用 foo() 函數。鑑於棧這種數據結構「先進後出」的特點,一般函數參數的入棧順序是從右至左的。
按照這樣的參數入棧順序,foo() 函數使用參數很方便,依次從棧中將參數取出就可以了。至於如何解析棧中的參數,則可以根據可變參數實現者指定的規則,例如在格式化字符串 fmt 中遇到 s 就解析為字符串等。
如果可變參數 foo() 接收到其他數目的參數,對於最終程序來說,也僅僅只需要修改壓棧的參數數目,其他並無太多不同。
小結
本文主要討論了C語言中可變參數函數的定義方法,以及計算機如何處理可變參數函數的過程,其實並不複雜。C語言不像C++那樣支持函數重載,但是藉助於可變參數函數和宏,我們可以像定義「偽類」那樣,定義自己的「偽函數重載」,這是一種編程技巧,以後有機會再討論了。
歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。
未經許可,禁止轉載。