可變參數的定義是類似這樣的:
void _cdecl myfun(char * fmt, ...){
...
}
這裏的fmt主要是爲了能夠識別後面到底有幾個參數及其類型的,否則編譯器是無法判斷函數參數個數的。
由於參數的個數可變,所以也只有c調用風格的函數可以實現它,因爲只有c調用風格的函數,參數的傳遞是由調用者負責的,而stdcall是由函數自身負責的。win32 api都是stdcall的。要想實現可變參數,那麼,必須有辦法取到第一個參數,並可以預知參數類型,從而通過遞增或者遞減地址,來逐個提取參數。
上述的遞增遞減的不同,取決於cpu。比如,如果cpu 的指令push是導致esp的棧指針指向內存的低端地址還是高端地址。我的機器push會導致esp的值變小,也就是低端生長。
加入我們有個函數f調用了myfun函數,那麼大致是這樣的
void f(){
int a1;
int a2;
myfun("abd", 'a', 4);
}
則函數調用出的反彙編情況,大致是:
push 4
push 'a'的ascii碼
push "abd"的內存地址
call myfun
add esp , 12 平衡一下棧操作,如果是stdcall就不會有這句了,相應的棧平衡會在函數myfun的內部的末尾處理,因爲stdcall可以根據函數聲明判斷應該pop多少來平衡棧。
所以,看到這裏,其實我們已經可以思考一下,也就知道該如何實現可變參數提取了,只要取得myfunc中的fmt參數地址(也就是push "abd"中壓入的地址),然後把地址的值增加,就可以依次取出 第二個參數地址。。。
比如myfunc(char * fmt, ...){
char * last_para_address = reinterpret<char*>(&fmt);//
last_para_address += 4;//這裏就是存放“abd”指針的那個參數的地址
last_para_address += 4;//這就是存放‘a'的那個參數的地址
。。。。。
所以,我們可以這樣實現
void _cdecl myprintf(char * fmt, ...){
char * vara_address = reinterpret_cast<char*>(&fmt);
#define getArgByType(t) ((sizeof(t) + sizeof(int) - 1)/sizeof(int)*sizeof(int))
//push,pop每次都是按照一個Int類型大小操作的,所以,比如char類型參數,入棧時也是壓入int大小的字節
char * local1 = NULL;
char * local2 = NULL;
int direction = 0;
if((&local1) > (&local2)){//判斷棧的增長方向,我的機器local1是先分配的變量,棧向着低端生長,所以&local1>&local2
direction = 1;//此時,由最後一個參數fmt向着內存高端遞增,便可以獲取聲明中第二個,第三個。。。參數了
}else{
direction = -1;
}
vara_address += getArgByType(fmt) * direction;
while(*fmt != '\0'){
switch(*fmt){
case 'c':
cout<<"char "<<*reinterpret_cast<char*>(vara_address)<<endl;//fmt的作用就是判斷每個參數類型,從而可以知道該移動多長的字節才能取到下一個參數地址
vara_address += getArgByType(char);
break;
case 'd':
cout<<"int "<<*reinterpret_cast<int*>(vara_address)<<endl;
vara_address += getArgByType(int);
break;
default:
vara_address += getArgByType(int);//這裏就是給default隨便做個處理,無關大雅
break;
}
++fmt;
}
}
這應該就是類似printf的實現機制了吧。
另外說明一點,獲取cpu的大端小端可以使用類似如下代碼:
union type{
char c[sizeof(int)];
int i;
};
type t ;
t.c[0] = 0x1;
t.c[1] = 0x2;
t.c[2] = 0x3;
t.c[3] = 0x4;
if(t.i == 0x04030201){//小端,就是通常我們的機器上的情況
}else{//大端 t.i == 0x01020304
}