原理解釋:linux
VA_LIST 是在C語言中解決變參問題的一組宏,在<stdarg.h>頭文件下。ios
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語言的方式來實現。post
va_list ap; //聲明一個變量來轉換參數列表
va_start(ap,fmt); //初始化變量
va_end(ap); //結束變量列表,和va_start成對使用
能夠根據va_arg(ap,type)取出參數 this
已經通過調試成功的輸出程序spa
注:32位機器適用操作系統
#include<stdio.h> #include <stdarg.h> #define bufsize 80 char buffer[bufsize]; int vspf(char *fmt, ...) { va_list argptr; int cnt; va_start(argptr, fmt); cnt = vsnprintf(buffer,bufsize ,fmt, argptr); va_end(argptr); return(cnt); } int main(void) { int inumber = 30; float fnumber = 90.0; char string[4] = "abc"; vspf("%d %f %s", inumber, fnumber, string); printf("%s\n", buffer); return 0; }
運行結果爲:
30 90.000000 abc
vsnprintf:int vsnprintf(char *str, size_t size, const char *format, va_list ap);
write output to character sting str
return value:the number of characters
printed (not including the trailing '\0' used to end output to strings). The functions snprintf() and vsnprintf() do not write more than size bytes (including the trailing '\0'). If the output was truncated due to this limit then the return value is the number of characters (not including the trailing '\0') which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated. If an output error is encountered, a negative
value is returned.
if (return_value > -1)
size = n+1;
else
size *= 2;
The glibc implementation of the functions snprintf() and vsnprintf() conforms to the C99 standard, i.e., behaves as described above, since glibc version 2.1. Until glibc 2.0.6 they would return -1 when the out put was truncated.
C語言用va_start等宏來處理這些可變參數。這些宏看起來很複雜,其實原理挺簡單,就是根據參
數入棧的特色從最靠近第一個可變參數的固定參數開始,依次獲取每一個可變參數的地址。下面咱們來分析這些宏。 在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)宏是爲了考慮那些內存地址須要對齊的系統,從宏的名字來應該是跟sizeof(int)對齊。通常的sizeof(int)=4,也就是參數在內存中的地址都爲4的倍數。好比,若是sizeof(n)在1-4之間,那麼_INTSIZEOF(n)=4;若是sizeof(n)在5-8之間,那麼_INTSIZEOF(n)=8。
爲了能從固定參數依次獲得每一個可變參數,va_start,va_arg充分利用下面兩點:
1. C語言在函數調用時,先將最後一個參數壓入棧
2. X86平臺下的內存分配順序是從高地址內存到低地址內存
高位地址
第N個可變參數
。。。
第二個可變參數
第一個可變參數 ap
固定參數 v
低位地址
由上圖可見,v是固定參數在內存中的地址,在調用va_start後,ap指向第一個可變參數。這個宏的做用就是在v的內存地址上增長v所佔的內存大小,這樣就獲得了第一個可變參數的地址。
接下來,能夠這樣設想,若是我能肯定這個可變參數的類型,那麼我就知道了它佔用了多少內存,依葫蘆畫瓢,我就能獲得下一個可變參數的地址。
讓我再來看看va_arg,它先ap指向下一個可變參數,而後減去當前可變參數的大小即獲得當前可變參數的內存地址,再作個類型轉換,返回它的值。
要肯定每一個可變參數的類型,有兩種作法,要麼都是默認的類型,要麼就在固定參數中包含足夠的信息讓程序能夠肯定每一個可變參數的類型。好比,printf,程序經過分析format字符串就能夠肯定每一個可變參數大類型。
最後一個宏就簡單了,va_end使得ap再也不指向有效的內存地址。
其實在varargs.h頭文件中定義了UNIX System V實行的va系列宏,而上面在stdarg.h頭文件中定義的是ANSI C形式的宏,這兩種宏是不兼容的,通常說來,咱們應該使用ANSI C形式的va宏。
定義_INTSIZEOF(n)主要是爲了某些須要內存的對齊的系統.C語言的函數是從右向左壓入堆棧的,函數的參數在堆棧中的分佈位置.我
們看到va_list被定義成char*,有一些平臺或操做系統定義爲void*.再看va_start的定義,定義爲&v+_INTSIZEOF(v),而&v是固定參數在堆棧的
地址,因此咱們運行va_start(ap, v)之後,ap指向第一個可變參數在堆棧的地址:
高地址|-----------------------------|
|函數返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n個參數(第一個可變參數) |
|-----------------------------|<--va_start後ap指向
|第n-1個參數(最後一個固定參數)|
低地址|-----------------------------|<-- &v
而後,咱們用va_arg()取得類型t的可變參數值,以上例爲int型爲例,咱們看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) ); 首先ap+=sizeof(int),已經指向下一個參數的地址了.而後返回
ap-sizeof(int)的int*指針,這正是第一個可變參數在堆棧裏的地址
而後用*取得這個地址的內容(參數值)賦給j.
高地址|-----------------------------|
|函數返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg後ap指向
|第n個參數(第一個可變參數) |
|-----------------------------|<--va_start後ap指向
|第n-1個參數(最後一個固定參數)|
低地址|-----------------------------|<-- &v
最後要說的是va_end宏的意思,x86平臺定義爲ap=(char*)0;使ap再也不指向堆棧,而是跟NULL同樣.有些直接定義爲((void*)0),這樣編譯器不
會爲va_end產生代碼,例如gcc在linux的x86平臺就是這樣定義的.在這裏你們要注意一個問題:因爲參數的地址用於va_start宏,因此參數不能聲明爲寄存器變量或做爲函數或數組類型.關於va_start, va_arg, va_end的描述就是這些了,咱們要注意的是不一樣的操做系統和硬件平臺的定義有些不一樣,但原理倒是類似的.
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語言的方式來實現
va_start()va_end()函數應用:
man:
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
1:當沒法列出傳遞函數的全部實參的類型和數目時,可用省略號指定參數表
void foo(...);
void foo(parm_list,...);
2:函數參數的傳遞原理
函數參數是以數據結構:棧的形式存取,從右至左入棧.
eg:
#include<iostream>
void fun(int a, ...) { int *temp = &a; temp++; for (int i = 0; i < a; ++i) { cout << *temp << endl; temp++; } } int main() { int a = 1; int b = 2; int c = 3; int d = 4; fun(4, a, b, c, d); system("pause"); return 0; }
Output::
1
2
3
4
3:獲取省略號指定的參數
在函數體中聲明一個va_list,而後用va_start函數來獲取參數列表中的參數,使用完畢後調用va_end()結束。
4.va_start使argp指向第一個可選參數。va_arg返回參數列表中的當前參數並使argp指向參數列表中的下一個參數。va_end把argp指針清爲NULL。函數體內能夠屢次遍歷這些參數,可是都必須以va_start開始,並以va_end結尾。
實例:
編寫vstart.c,以下:
//vstart.c #include <stdio.h> #include <strings.h> #include <stdarg.h> int demo(char *fmt, ...); int main() { demo("DEMO", "This", "is", "a", "demo!", ""); return 0; } int demo( char *fmt, ... ) { va_list argp; int argno = 0; char *para; va_start(argp, fmt); while (1) { para = va_arg(argp, char *); if (strcmp( para, "") == 0) break; printf("Parameter #%d is: %s/n", argno, para); argno++; } va_end( argp ); return 0; }
運行結果以下:
Parameter #0 is: ThisParameter #1 is: isParameter #2 is: aParameter #3 is: demo!