問題
當我們剛開始學習C語言的時候,就接觸到printf()函數,可是當時“道行”不深或許不夠細心留意,又或者我們理所當然地認為庫函數規定這樣就是這樣,沒有發現這個函數與普通的函數存在區別,普通函數的參數在函數定義的時候就確定,而printf()函數的參數列表在調用時可變。還有一個原因導致我們沒有去關注這個函數的實現,就是在編程的過程中很少用到參數列表可變的函數。的確是這樣的,但是如果可以理解並內化,這將在編程過程中對某些功能實現帶來很大的幫助。比如,在嵌入式設備開發中,可以利用設備的接口(UART、USB)編寫出一套類似printf()函數功能的調試工具。
printf()函數的實現
static char sprint_buf[1024];
int printf(char *fmt, ...)
{
va_list args;
int n;
va_start(args, fmt);
n = vsprintf(sprint_buf, fmt, args);
va_end(args);
write(stdout, sprint_buf, n);
return n;
}
咋一看,printf()函數的試下也不是太復雜,有幾個陌生又關鍵的詞語va_list、va_start、va_end、vsprintf。其實還有一個va_arg(stdarg.h),是參數列表可變的關鍵,而真正的打印實現是vsprintf()函數,在這里不討論vsprintf()函數,只討論三個宏定義。
先看看stdarg.h文件下這三個宏的實現:
第一種:
typedef char *va_list;
#define va_start(ap,v) ap = (va_list)&v + sizeof(v)
#define va_arg(ap,t) (((t *)ap)++[0])
#define va_end(ap)
第二種:
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,type) (*(type *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
_INTSIZEOF(n):為了字節對齊,將n的長度化為int長度的整數倍。
在32位系統中,~(sizeof(int)–1) )展開為~(4-1)=~(00000011b)=11111100b,這樣任何數& ~(sizeof(int)–1) )后最后兩位肯定為0,也就是4的整數倍。
每個平台下的stdarg.h頭文件的定義都不相同,但是意思都一樣:
(1) va_list: 定義一個va_list型的變量ap,也就是char *;
(2) va_start:獲取到可變參數表的首地址,並將該地址賦給指針ap;
(3) va_arg: 獲取當前ap所指向的可變參數,並將指針ap指向下一個可變參數。注意,該宏的第二個參數為類型;
(4) va_end: 結束可變參數的獲取。
應用
當理解了printf()函數的實現,便可以着手去實現嵌入式設備中的調試工具,其原理一樣:
int bsp_debug_printf(const char *fmt, ...)
{
int ret = -1;
va_list ap;
char buf[256];
va_start(ap, fmt);
(void)vsnprintf((char *)buf, sizeof(buf), fmt, ap);
va_end(ap);
bsp_puts(buf);
ret = 0;
return ret;
}
以上只是簡單的實現這個功能,並沒有考慮硬件保護,返回打印字符數等。
再看一例:
找出最大值
int MaxTest(int c, ...)
{
int max = c;
va_list ap;
va_start(ap,c);
c = va_arg(ap,int);
while(0 != c)
{
if(max < c) max = c;
c = va_arg(ap,int);
}
va_end(ap);
return max;
}
這個例子主要想利用va_list、va_start、va_arg、va_end這四個關鍵指令來實現可變參數列表的函數,它需要一個參數列表末尾標識(0)來告訴系統這參數列表的最后一個參數。
還可以有另外一種方法來實現以上功能:
int MaxTest1(int n, ...)
{
int max = n;
int *p = &n + 1;
while(0 != *p)
{
if (max < *p) max = *p;
p++;
}
return max;
}
這個例子利用了函數參數的存儲規則來實現對參數的獲取。可變參數函數的實現與函數調用的棧結構有關,正常情況下C/C++的函數參數入棧規則從右到左的,即函數中的最右邊的參數最先入棧。
這種方法看似效果相同,但是稍有不慎就會帶來災難。若printf()函數由這種方法實現,而沒有邊界的檢查,當堆棧越界訪問時極可能導致程序崩潰。