最近在蛋痛的可變參數中緋徊....
要注意的是:由於參數的地址用於VA_START宏,所以參數不能聲明為寄存器變數,或作為函數或數群組類型。
使用VA_LIST應該注意的問題:
(1)因為va_start, va_arg, va_end等定義成宏,所以它顯得很愚蠢,可變參數的類型和個數完全在該函數中由程式碼控制,它並不能智能地識別不同參數的個數和類型. 也就是說,你想實現智能識別可變參數的話是要通過在自己的程式裡作判斷來實現的.
(2)另外有一個問題,因為編譯器對可變參數的函數的原型檢查不夠嚴格,對編程查錯不利.不利於我們寫出高品質的代碼。
小結:可變參數的函數原理其實很簡單,而VA系列是以宏定義來定義的,實現跟堆棧相關。我們寫一個可變函數的C函數時,有利也有弊,所以在不必要的場合,我們無需用到可變參數,如果在C++裡,我們應該利用C++多態性來實現可變參數的功能,盡量避免用C語言的方式來實現。
全文摘
http://blog.sina.com.cn/s/blog_477815290100cxtk.html
原理解釋:
VA_LIST 是在C語言中解決變參問題的一組宏,在<stdarg.h>標頭檔下。
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 ap; //聲明一個變數來轉換參數列表
va_start(ap,fmt); //初始設定變數
va_end(ap); //結束變數列表,和va_start成對使用
可以根據va_arg(ap,type)取出參數
已經經過調試成功的輸出程式
#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;
}
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語言的方式來實現
另外一個說法從CSDN上的..
int snprintf(char *str, size_t size, const char *format, ...);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
=
區別大了
snprintf裡面可以使用變參
vsnprintf幾乎不會被直接使用,而是在不定參數的函數內部調用