1、C 語言中可變参數的用法va_list、va_start 、va_arg、va_end 参數定義C 語言可變参簡介 我們在 C 語言編程中會遇到一些参數個數可變的函數 ,例如 printf()這個函數,它的定義是這样的: int printf C 語言可變参簡介我們在 C 語言編程中會遇到一些参數個數可變的函數, 例如 printf()這個函數,它的定義是這样的:int printf( const char* format, .);它除了有一個参數 format 固定以外,後面跟的参數的個數和類型是可變的 ,例如我們可以有以下不同的調用方法: printf(“%d“,i); printf(“%
2、s“,s); printf(“the number is %d ,string is:%s“, i, s); 寫一個簡單的可變参數的 C 函數 下面我們來探討如何寫一個簡單的可變参數的 C 函數. 寫可變参數的 C 函數要在程序中用到以下這些宏:void va_start( va_list arg_ptr, prev_param ); type va_arg( va_list arg_ptr, type ); void va_end( va_list arg_ptr );va 在這裏是 variable-argument(可變参數)的意思.這些宏定義在 stdarg.h 中,所以用到可變参數的
3、程序應該包含這個頭文件.下面我們寫一個簡單的可變参數的函數 ,改函數至少有一個整數参數,第二個参數也是整數,是可選的.函數只是打印這兩個参數的值.void simple_va_fun(int i, .) va_list arg_ptr; int j=0; va_start(arg_ptr, i); j=va_arg(arg_ptr, int); va_end(arg_ptr); printf(“%d %dn“, i, j); return; 我們可以在我們的頭文件中這样聲明我們的函數: extern void simple_va_fun(int i, .);我們在程序中可以這样調用: simp
4、le_va_fun(100); simple_va_fun(100,200);從這個函數的實現可以看到,我們使用可變参數應該有以下步驟 : 1)首先在函數裏定義一個 va_list 型的變量,這裏是 arg_ptr,這個變量是指向参數的指針. 2)然後用 va_start 宏初始化變量 arg_ptr,這個宏的第二個参數是第一個可變参數的前一個参數,是一個固定的参數. 3)然後用 va_arg 返回可變的参數,並賦值给整數 j. va_arg 的第二個参數是你要返回的参數的類型,這裏是 int 型.4)最後用 va_end 宏結束可變参數的獲取.然後你就可以在函數裏使用第二個参數了 .如果函數
5、有多個可變参數的,依次調用 va_arg 獲取各個参數 .如果我們用下面三種方法調用的話,都是合法的,但結果卻不一样: 1) simple_va_fun(100);結果是:100 -123456789(會變的值) 2) simple_va_fun(100,200);結果是:100 200 3) simple_va_fun(100,200,300);結果是:100 200 我們看到第一種調用有錯誤,第二種調用正確,第三種調用盡管結果正確,但和我們函數最初的設計有沖突.下面我們探討出現這些結果的原因和可變参數在編譯器中是如何處理的。 可變参數在編譯器中的處理 我們知道 va_start,va_ar
6、g,va_end 是在 stdarg.h 中被定義成宏的,由於 1)硬件平台的不同 2)編譯器的不同 ,所以定義的宏也有所不同, 下面以 VC+中 stdarg.h 裏 x86 平台的宏定義摘錄如下(號表示折行): typedef char * va_list; #define _INTSIZEOF(n) (sizeof(n)+sizeof(int)-1)首先 ap+=sizeof(int),已經指向下一個参數的地址了. 然後返回 ap-sizeof(int)的 int*指針,這正是第一個可變参數在堆棧裏的地址(圖 2).然後用*取得這個地址的內容(参數值) 賦给 j.高地址|-| |函數返回
7、地址 | |-| |. | |-| -va_arg 後 ap 指向 |第 n 個参數( 第一個可變参數 ) | |-| -va_start 後 ap 指向 |第 n-1 個参數(最後一個固定参數 )| 低地址|-| - 使 ap 不再指向堆棧, 而是跟 NULL 一样.有些直接定義为(void*)0),這样編譯器不會为 va_end 產生代碼,例如 gcc在 linux 的 x86 平台就是這样定義的 .在這裏大家要注意一個問題: 由於参數的地址用於va_start 宏,所以参數不能聲明为寄存器變量或作为函數或數組類型.關於 va_start, va_arg, va_end 的描述就是這些了,
8、 我們要注意的是不同的操作系統和硬件平台的定義有些不同, 但原理卻是相似的. 可變参數在編程中要注意的問題 因为 va_start, va_arg, va_end 等定義成宏,所以它顯得很愚蠢, 可變参數的類型和個數完全在該函數中由程序代碼控制,它並不能智能地識別不同参數的個數和類型 .有人會問:那麼printf 中不是實現了智能識別参數嗎?那是因为函數 printf 是從固定参數 format 字符串來分析出参數的類型,再調用 va_arg 的來獲取可變参數的.也就是說,你想實現智能識別可變参數的話是要通過在自己的程序裏作判斷來實現的.另外有一個問題 ,因为編譯器對可變参數的函數的原型檢查不
9、夠嚴格,對編程查錯不利.如果 simple_va_fun()改为: void simple_va_fun(int i, .) va_list arg_ptr; char *s=NULL; va_start(arg_ptr, i); s=va_arg(arg_ptr, char*); va_end(arg_ptr); printf(“%d %sn“, i, s); return; 可變参數为 char*型,當我們忘記用兩個参數來調用該函數時,就會出現 core dump(Unix) 或者頁面非法的錯誤(window 平台). 但也有可能不出錯, 但錯誤卻是難以發現,不利於我們寫出高質量的程序.
10、以下提一下 va 系列宏的兼容性.System V Unix 把 va_start 定義为只有一個参數的宏: va_start(va_list arg_ptr);而 ANSI C 則定義为: va_start(va_list arg_ptr, prev_param);如果我們要用 system V 的定義,應該用 vararg.h 頭文件中所定義的宏,ANSI C 的宏跟system V 的宏是不兼容的,我們一般都用 ANSI C,所以用 ANSI C 的定義就夠了, 也便於程序的移植. 小結: 1、標准 C 庫的中的三個宏的作用只是用來確定可變参數列表中每個参數的內存地址,編譯器是不知道参數
11、的實際數目的。2、在實際應用的代碼中,程序員必須自己考慮確定参數數目的辦法,如在固定参數中設標志- printf 函數就是用這個辦法。在預先設定一個特殊的結束標記,就是說多輸入一個可變参數,調用時要將最後一個可變参數的值設置成這個特殊的值,在函數體中根據這個值判斷是否達到参數的結尾。本文前面的代碼就是采用這個辦法.無論采用哪種辦法,程序員都應該在文檔中告訴調用者自己的約定。3、實現可變参數的要點就是想辦法取得每個参數的地址,取得地址的辦法由以下幾個因素决定:函數棧的生長方向参數的入棧順序CPU 的對齊方式內存地址的表達方式結合源代碼,我們可以看出 va_list 的實現是由决定的,_INTSIZEOF(n)的引入則是由决定的,他和又一起决定了 va_start 的實現,最後 va_end 的存在則是良好編程風格的體現,將不再使用的指針設为 NULL,這样可以防止以後的誤操作。4、取得地址後,再結合参數的類型,程序員就可以正確的處理参數了。理解了以上要點,相信稍有經驗的讀者就可以寫出适合於自己機器的實現來。可變参數的函數原理其實很簡單,而 va 系列是以宏定義來定義的,實現跟堆棧相關. 我們寫一個可變函數的 C 函數時,有利也有弊,所以在不必要的場合, 我們無需用到可變参數.如果在 C+裏,我們應該利用 C+的多態性來實現可變参數的功能,盡量避免用 C 語言的方式來實現.