讀Linux內核中的vsprintf函數的時候遇到了C語言的可變參數調用,查了挺多資料還是這篇比較詳細,而且自己驗證了下,確實如此
(一)寫一個簡單的可變參數的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中,所以用到可變參數的程序應該包含這個 頭文件.下面我們寫一個簡單的可變參數的函數,改函數至少有一個整數 參數,第二個參數也是整數,是可選的.函數只是打印這兩個參數的值. 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 %d\n", i, j); return; } 我們可以在我們的頭文件中這樣聲明我們的函數: extern void simple_va_fun(int i, ...); 我們在程序中可以這樣調用: simple_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宏結束可變參數的獲取.然後你就可以在函數裡使 用第二個參數了.如果函數有多個可變參數的,依次調用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_arg,va_end是在stdarg.h中被定義成宏的, 由於1)硬件平台的不同 2)編譯器的不同,所以定義的宏也有所不同,下 面以VC++中stdarg.h裡x86平台的宏定義摘錄如下('\'號表示折行): typedef char * va_list; #define _INTSIZEOF(n) \ ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) \ ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 ) 定義_INTSIZEOF(n)主要是為了某些需要內存的對齊的系統.C語言的函 數是從右向左壓入堆棧的,圖(1)是函數的參數在堆棧中的分布位置.我 們看到va_list被定義成char*,有一些平台或操作系統定義為void*.再 看va_start的定義,定義為&v+_INTSIZEOF(v),而&v是固定參數在堆棧的 地址,所以我們運行va_start(ap, v)以後,ap指向第一個可變參數在堆 棧的地址,如圖: 高地址|-----------------------------| |函數返回地址 | |-----------------------------| |....... | |-----------------------------| |第n個參數(第一個可變參數) | |-----------------------------|<--va_start後ap指向 |第n-1個參數(最後一個固定參數)| 低地址|-----------------------------|<-- &v 圖( 1 ) 然後,我們用va_arg()取得類型t的可變參數值,以上例為int型為例,我 們看一下va_arg取int型的返回值: j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) ); 首先ap+=sizeof(int),已經指向下一個參數的地址了.然後返回 ap-sizeof(int)的int*指針,這正是第一個可變參數在堆棧裡的地址 (圖2).然後用*取得這個地址的內容(參數值)賦給j. 高地址|-----------------------------| |函數返回地址 | |-----------------------------| |....... | |-----------------------------|<--va_arg後ap指向 |第n個參數(第一個可變參數) | |-----------------------------|<--va_start後ap指向 |第n-1個參數(最後一個固定參數)| 低地址|-----------------------------|<-- &v 圖( 2 ) 最後要說的是va_end宏的意思,x86平台定義為ap=(char*)0;使ap不再 指向堆棧,而是跟NULL一樣.有些直接定義為((void*)0),這樣編譯器不 會為va_end產生代碼,例如gcc在linux的x86平台就是這樣定義的. 在這裡大家要注意一個問題:由於參數的地址用於va_start宏,所 以參數不能聲明為寄存器變量或作為函數或數組類型. 關於va_start, va_arg, va_end的描述就是這些了,我們要注意的 是不同的操作系統和硬件平台的定義有些不同,但原理卻是相似的. (三)可變參數在編程中要注意的問題 因為va_start, va_arg, va_end等定義成宏,所以它顯得很愚蠢, 可變參數的類型和個數完全在該函數中由程序代碼控制,它並不能智能 地識別不同參數的個數和類型. 有人會問:那麼printf中不是實現了智能識別參數嗎?那是因為函數 printf是從固定參數format字符串來分析出參數的類型,再調用va_arg 的來獲取可變參數的.也就是說,你想實現智能識別可變參數的話是要通 過在自己的程序裡作判斷來實現的. 另外有一個問題,因為編譯器對可變參數的函數的原型檢查不夠嚴 格,對編程查錯不利.如果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 %s\n", i, s); return; } 可變參數為char*型,當我們忘記用兩個參數來調用該函數時,就會出現 core dump(Unix) 或者頁面非法的錯誤(window平台).但也有可能不出 錯,但錯誤卻是難以發現,不利於我們寫出高質量的程序. 以下提一下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的定義就夠了,也便於程序的移植. 小結: 可變參數的函數原理其實很簡單,而va系列是以宏定義來定義的,實 現跟堆棧相關.我們寫一個可變函數的C函數時,有利也有弊,所以在不必 要的場合,我們無需用到可變參數.如果在C++裡,我們應該利用C++的多 態性來實現可變參數的功能,盡量避免用C語言的方式來實現.
===================================================
概述 由於在C語言中沒有函數重載,解決不定數目函數參數問題變得比較麻煩;即使采用C++,如果參數個數不能確定,也很難采用函數重載.對這種情況,有些人采用指針參數來解決問題.下面就c語言中處理不定參數數目的問題進行討論. 定義 大家先看幾宏. 在VC++6.0的include有一個stdarg.h頭文件,有如下幾個宏定義: #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //第一個可選參數地址 #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一個參數地址 #define va_end(ap) ( ap = (va_list)0 ) // 將指針置為無效 如果對以上幾個宏定義不理解,可以略過,接這看後面的內容. 參數在堆棧中分布位置 在進程中,堆棧地址是從高到低分配的.當執行一個函數的時候,將參數列表入棧,壓入堆棧的高地址部分,然後入棧函數的返回地址,接著入棧函數的執行代碼,這個入棧過程,堆棧地址不斷遞減,一些黑客就是在堆棧中修改函數返回地址,執行自己的代碼來達到執行自己插入的代碼段的目的. 總之,函數在堆棧中的分布情況是:地址從高到低,依次是:函數參數列表,函數返回地址,函數執行代碼段. 堆棧中,各個函數的分布情況是倒序的.即最後一個參數在列表中地址最高部分,第一個參數在列表地址的最低部分.參數在堆棧中的分布情況如下: 最後一個參數 倒數第二個參數 ... 第一個參數 函數返回地址 函數代碼段 示例代碼 void arg_test(int i, ...); int main(int argc,char *argv[]) { int int_size = _INTSIZEOF(int); printf("int_size=%d\n", int_size); arg_test(0, 4);
arg_cnt(4,1,2,3,4); return 0; } void arg_test(int i, ...) { int j=0; va_list arg_ptr;
va_start(arg_ptr, i); printf("&i = %p\n", &i);//打印參數i在堆棧中的地址 printf("arg_ptr = %p\n", arg_ptr); //打印va_start之後arg_ptr地址, //應該比參數i的地址高sizeof(int)個字節 //這時arg_ptr指向下一個參數的地址
j=*((int *)arg_ptr); printf("%d %d\n", i, j); j=va_arg(arg_ptr, int); printf("arg_ptr = %p\n", arg_ptr); //打印va_arg後arg_ptr的地址 //應該比調用va_arg前高sizeof(int)個字節 //這時arg_ptr指向下一個參數的地址 va_end(arg_ptr); printf("%d %d\n", i, j); } 代碼說明: int int_size = _INTSIZEOF(int);得到int類型所占字節數 va_start(arg_ptr, i); 得到第一個可變參數地址,根據定義(va_list)&v得到起始參數的地址, 再加上_INTSIZEOF(v) ,就是其實參數下一個參數的地址,即第一個可變參數地址. j=va_arg(arg_ptr, int); 得到第一個參參數的值,並且arg_ptr指針上移一個_INTSIZEOF(int),即指向下一個可變參數的地址. va_end(arg_ptr);置空arg_ptr,即arg_ptr=0; 總結:讀取可變參數的過程其實就是堆棧中,使用指針,遍歷堆棧段中的參數列表,從低地址到高地址一個一個地把參數內容讀出來的過程. 在編程中應該注意的問題和解決辦法 雖然可以通過在堆棧中遍歷參數列表來讀出所有的可變參數,但是由於不知道可變參數有多少個,什麼時候應該結束遍歷,如果在堆棧中遍歷太多,那麼很可能讀取一些無效的數據. 解決辦法:a.可以在第一個起始參數中指定參數個數,那麼就可以在循環還中讀取所有的可變參數;b.定義一個結束標記,在調用函數的時候,在最後一個參數中傳遞這個標記,這樣在遍歷可變參數的時候,可以根據這個標記結束可變參數的遍歷; 下面是一段示例代碼: //第一個參數定義可選參數個數,用於循環取初參數內容 void arg_cnt(int cnt, ...); int main(int argc,char *argv[]) { int int_size = _INTSIZEOF(int); printf("int_size=%d\n", int_size); arg_cnt(4,1,2,3,4); return 0; } void arg_cnt(int cnt, ...) { int value=0; int i=0; int arg_cnt=cnt; va_list arg_ptr; va_start(arg_ptr, cnt); for(i = 0; i < cnt; i++) { value = va_arg(arg_ptr,int); printf("value%d=%d\n", i+1, value); } }
雖然可以根據上面兩個辦法解決讀取參數個數的問題,但是如果參數類型都是不定的,該怎麼辦,如果不知道參數的類型,即使讀到了參數也沒有辦法進行處理.解決辦法:可以自定義一些可能出現的參數類型,這樣在可變參數列表中,可以可變參數列表中的那類型,然後根據類型,讀取可變參數值,並進行准確地轉換.傳遞參數的時候可以這樣傳遞:參數數目,可變參數類型1,可變參數值1,可變參數類型2,可變參數值2,.... 這裡給出一個完整的例子: #include <stdio.h> #include <stdarg.h> const int INT_TYPE = 100000; const int STR_TYPE = 100001; const int CHAR_TYPE = 100002; const int LONG_TYPE = 100003; const int FLOAT_TYPE = 100004; const int DOUBLE_TYPE = 100005; //第一個參數定義可選參數個數,用於循環取初參數內容 //可變參數采用arg_type,arg_value...的形式傳遞,以處理不同的可變參數類型 void arg_type(int cnt, ...); //第一個參數定義可選參數個數,用於循環取初參數內容 void arg_cnt(int cnt, ...); //測試va_start,va_arg的使用方法,函數參數在堆棧中的地址分布情況 void arg_test(int i, ...); int main(int argc,char *argv[]) { int int_size = _INTSIZEOF(int); printf("int_size=%d\n", int_size); arg_test(0, 4);
arg_cnt(4,1,2,3,4); arg_type(2, INT_TYPE, 222, STR_TYPE, "ok,hello world!"); return 0; } void arg_test(int i, ...) { int j=0; va_list arg_ptr;
va_start(arg_ptr, i); printf("&i = %p\n", &i);//打印參數i在堆棧中的地址 printf("arg_ptr = %p\n", arg_ptr); //打印va_start之後arg_ptr地址, //應該比參數i的地址高sizeof(int)個字節 //這時arg_ptr指向下一個參數的地址
j=*((int *)arg_ptr); printf("%d %d\n", i, j); j=va_arg(arg_ptr, int); printf("arg_ptr = %p\n", arg_ptr); //打印va_arg後arg_ptr的地址 //應該比調用va_arg前高sizeof(int)個字節 //這時arg_ptr指向下一個參數的地址 va_end(arg_ptr); printf("%d %d\n", i, j); } void arg_cnt(int cnt, ...) { int value=0; int i=0; int arg_cnt=cnt; va_list arg_ptr; va_start(arg_ptr, cnt); for(i = 0; i < cnt; i++) { value = va_arg(arg_ptr,int); printf("value%d=%d\n", i+1, value); } } void arg_type(int cnt, ...) { int arg_type = 0; int int_value=0; int i=0; int arg_cnt=cnt; char *str_value = NULL; va_list arg_ptr; va_start(arg_ptr, cnt); for(i = 0; i < cnt; i++) { arg_type = va_arg(arg_ptr,int); switch(arg_type) { case INT_TYPE: int_value = va_arg(arg_ptr,int); printf("value%d=%d\n", i+1, int_value); break; case STR_TYPE: str_value = va_arg(arg_ptr,char*); printf("value%d=%d\n", i+1, str_value); break; default: break; } } } =======================================================================
有關VA_LIST的用法:
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語言的方式來實現。
==========================================================================
變長參數應用舉例:
先得聲明一個變長參數的變量va_list list 在使用前要先用va_start(list, last_param)對list進行初始化,last_param為最右邊的已知參數,表示list 從last_param的下一個參數開始 va_arg(list, 類型) 最後不要忘了用va_end(list)
eg1: #include<iostream> #include<iomanip> #include<stdarg.h>
using namespace std;
double average(int, ...);
int main() { double w = 37.5, x = 22.5, y = 1.7, z = 10.2;
cout << setiosflags(ios::fixed | ios::showpoint) << setprecision(1) << "w = " << w << "\nx = " << x << "\ny = " << y << "\nz = " << z << endl;
cout << average(2, w, x) << endl; cout << average(3, w, x, y) << endl; cout << average(4, w, x, y, z) << endl;
return 0; }
double average(int i, ...) { double total = 0; va_list ap;
va_start(ap, i);
for(int j = 1; j <= i; j++) { total += va_arg(ap, double); }
va_end( ap ); return total/i; }
eg2: #include<iostream.h> #include <stdlib.h> #include <stdarg.h> void error(const char*format...); void main() { int a; char c='d'; char s[100]; error("Enter a string:"); //輸入一個字符串 cin>>s; error("Enter an integer:"); //輸入一整數 cin>>a; error("%s\n%d\n%c\n",s,a,c); //打印輸出
} void error(const char*format...) //實現像printf函數一樣的打印輸出功能 { int i; int j=0; va_list ap; va_start(ap,format); for(i=0;*(format+i)!=0;) { int in; char* pc; char d; if(*(format+i)=='%') { switch(*(format+i+1)) { case'd':in=va_arg(ap,int);cout<<in;i=i+2;break; case's':pc=va_arg(ap,char*);cout<<pc;i=i+2;break; case'c':d=va_arg(ap,char);cout<<d;i=i+2;break; default:cout<<'%';i=i+1;break; } } else { cout<<*(format+i); i++; }
} }
================================================================
C++變長參數函數的用法
書上說,當無法列出傳遞函數的所有實參的類型和數目時,可用省略號指定參數表 (...)
如:void foo(...); void foo(parm_list,...); void foo(...) { //... } 調用:foo(a,b,c);
就是不懂,把a,b,c的值傳進函數裡面後,用什麼變量來接收???如果不能接收,(...)豈不是沒意義? 還有就是不明白 int printf(const char*...); printf("hello,&s\n",userName);
這個c的輸出函數是怎麼用(...)實現的.
首先函數體中聲明一個va_list,然後用va_start函數來獲取參數列表中的參數,使用完畢後調用va_end()結束。像這段代碼: void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...) { va_list args; va_start(args, pszFormat); _vsnprintf(pszDest, DestLen, pszFormat, args); va_end(args); }
===========================================================
va_list的用法 還記得printf函數調用的時候那個“...”嗎?就是可以輸入任意的參數。現在你用va_list也可以實現類似的函數聲明,printf就是這樣做的。
va_list args; //聲明變量 va_start(args, before); //開始解析。args指向before後面的參數 參數類型 var = va_arg(args, 參數類型); //取下一個參數並返回。args指向下一個參數 va_end(args);