C語言中有些函數使用可變參數,比如常見的int printf( const char* format, ...),第一個參數format是固定的,其余的參數的個數和類型都不固定。
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不再指向有效的內存地址。
看了這幾個宏,不禁讓我再次感慨,C語言太靈活了,而且代碼可以寫得非常簡潔,雖然有時候讓人看得不是很明白,但是一旦明白 過來,你肯定會為它擊掌叫好!
其實在varargs.h頭文件中定義了UNIX System V實行的va系列宏,而上面在stdarg.h頭文件中定義的是ANSI C形式的宏,這兩種宏是不兼容的,一般說來,我們應該使用ANSI C形式的va宏。