可变参数的定义是类似这样的:
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
}