我們對現實生活中很多事情已習以為常,比如娛樂圈的潛規則,中國足球的假球,土的掉渣的printf()。殊不知這背後蘊藏著重重玄機,復雜的機制。現在我們就試著即開printf的神秘面紗。
學習c之後很多年都沒想過printf有那麼多不同之處,可變參數函數的實現一點都不簡單,就像沒有無緣無故的愛,它的實現不是天然的。
先看看,c為可變參函數提供的幾個利器,以宏的形式實現。
#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 ) // 將指針置為無效
va_list用於定義一個變量獲取可變參數指針
va_start用於將va_list定義的指針進行初始化
va_arg用於獲取對應指針的真實類型數據
va_end用於清空va_list定義的指針
擒賊先擒王,在這裡只解決va_arg,別的不需懼怕。曾經,我看不懂這個。看懂之後感受到了造物主的偉大。
ap+=_INTSIZEOF(t);這裡就使得ap指向了下一個可變參數的起始地址。然後
ap+=_INTSIZEOF(t)-_INTSIZEOF(t)整個表達式的結果回到當前這個可變參數的起始地址,但是注意到ap已經指向下一個可變參數地址了。這就方便後續的處理 。
我們有必要了解一下C函數的調用規則了,在調用一個函數之前,調用方會將這個函數參數push(修改ESP指針),並且push規則是先push最後一個參數,最後push第一個參數,因此ESP指針最後應該是指向第一個參數。可變參數就是利用了這一點,一旦獲取到第一個參數的地址後,就能夠通過地址向前查找所有的參數。(注意:x86上的堆棧是反向的,push會使ESP的值減少,而不是增加)。
看到這裡,相信大家應該可以編寫一個簡單的可變參數函數了。我還是直接copy一個,呵呵,關鍵時候要的就是穩健,請主寬恕我。
#include<stdio.h>
#include<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);
} int main(void)
{
simple_va_fun(100);
simple_va_fun(100,200);
simple_va_fun(100,200,300);
return 0;
}
只有第二個是正確的,其實答案可想而知,這跟我們所寫的程序有關。由於沒有類型和參數的檢查,最多也就只能這樣了。那為什麼人家的printf便可以辨識呢??那是因為函數printf是從固定參數format字符串來分析出參數的類型,再調用va_arg的來獲取可變參數的。這下應該理清楚了。
可變參數宏就不說了吧,簡單的畫個妝而已,不過依然很強大,呵呵。