今天在工作之余,突然有个朋友说到由于工作需要,需要用到C的变参。这个呢,以前大学里面研究过,实习的时候也用到过,但是没有怎么深入,这次我决定深入的去了解一下C的变参是怎么实现的。
首先看了printf的源代码。下面将以printf为例子来说明。毕竟我们最先接触的函数里面就有这个函数。
先贴实现代码,看了再说。(这里我只解析 几种变量,其他的解析是一样的。)
#define MY_INSIZEOF(n) ((sizeof(n) + sizeof(int) - 1)& ~(sizeof(int) - 1))
#define my_va_list char *
#define my_va_start(v, va) v = (my_va_list)&va + MY_INSIZEOF(va);
#define my_va_end(v) v = 0
#define my_va_arg(v, vsize) *(vsize *)((v += MY_INSIZEOF(vsize)) - MY_INSIZEOF(vsize))
void my_printf(char *format, ...)
{
//va_list
my_va_list list;
my_va_start(list, format);
while(*format != NULL)
{
if (*format == '%')
{
if (*(format + 1) == 'c')
{
char c = my_va_arg(list, char);
putc(c, stdout);
}
else if(*(format + 1) == 's')
{
char* pStr = my_va_arg(list, char*);
puts(pStr);
}
else if(*(format + 1) == 'd')
{
int nVal = my_va_arg(list, int);
char buf[12];
memset(buf, 0, sizeof(buf));
itoa(nVal, buf, 10);
puts(buf);
}
else if(*(format + 1) == 'f')
{
double fVal;
fVal = my_va_arg(list, double);//这里不能直接写float型,后面有介绍
char buf[22];
memset(buf, 0, sizeof(buf));
gcvt((float)fVal, 10, buf);
puts(buf);
}
else
{
putc(*format, stdout);
--format;
}
format++;
}
else putc(*format, stdout);
format++;
}
my_va_end(list);
}
都知道函数调用的时候,参数传递的方式主要有2种,一种是通过将参数压入堆栈的形式,一种是通过放入寄存器,但最常用的还是将参数压入堆栈。而基于_stdcall调用,参数压栈方式是从右到坐的方式。即各个参数的地址是紧挨着的,通过地址的操作可以访问到下一个参数的地址。上面是通过操作指针来实现访问参数,后面还有一种通过汇编来实现参数解析
my_va_start的作用是取第一个参数的下一个参数的地址
由于不同的参数类型,所占的内存空间大小不一样,那么也就造成了在访问内存时应该向后移几位的问题,而且由于系统为了访问快速,做参数做了字节对齐,这里的MY_INSIZEOF即是根据参数的大小做内存对齐,不然可能访问的地址并不是真正的参数的地址,或许错位了。
my_va_list 此宏主要是用于操作内存单元,我们都知道,每一个字节为8位,而刚好每一个char也是8位,那么这里也就是为了让在访问对应内存时能一字节一字节的访问。
my_va_arg这个宏很简单,由于我们在函数开始执行时,list已经指向了函数的第二个参数。这个宏的大致意思就是,将参数强制转换为我们需要的类型,并将list指向下一个参数的地址(可以理解为汇编中的esp)。
上面的是通过C的方式实现,看起来有点难懂,特别是宏,下面以汇编的形式来解析参数,很好明白。(先看下面的汇编,再去看上面的C代码方式,将会简单多了)(这里以输出一个字符为例,输出其他参数是一样,只是我们在读出内存中的值的同时,根据我们需要的参数解析即可)
void my_printf2(char *format, ...)
{
while(*format != NULL)
{
if (*format == '%')
{
if (*(format + 1) == 'c')
{
char dst;
_asm
{
push eax ;保存eax
push ebx ;保存ebx
push ecx ;保存ecx
mov eax, esp ;将esp的值放入eax中
lea ebx,format;将format的地址读入ebx (即在栈中的位置)
mov esp, ebx;将esp指向format在栈中的值,即指向第一个参数的位置
pop ebx;弹出第一个参数;这个参数即是format
pop ecx;弹出第二个参数,即这里是我们需要的参数
mov dst, cl;由于一个字符只占用1个字节,存放在低地址cl中,将cl中的字符赋值到dst中
;push ecx;这两句可要可不要,因为后面直接修改了esp指针,为了可读性可以加上,
;push ebx
mov esp,eax;后面的都是还原环境
pop ecx
pop ebx
pop eax
}
putc(dst, stdout);
}
else
{
putc(*format, stdout);
--format;
}
++format;
}
++format;
}
}
大部分代码和上面的都差不多,而且这个代码看着就少多了。上面汇编有注释,这里也就不多解释了,我感觉用汇编来写,代码又简洁又易懂。简单多了。
上面还有值得注意的便是第一种方法在解析float型的时候,不能写float型,因为系统在传参数的时候,直接传的double型,即我们要按照double型来解析,不然就解析的数据不对了,这里我就不贴图了,如果想自己证明的话,很简单,直接看内存的值即可。还有,在传递浮点数的时候,如果是直接写的参数的话,必须强制转为为float或者double才能正确输出,例如;printf("%f",123),那么这样打印出来的值其实是不对的,原因就是当你直接写123的时候,系统并不知道你想以float的形式保存,那么系统将会自动以整形的形式存储在内存中,但是我们在解析的时候,确是以浮点数的形式去解析,那么将导致解析的值不对。所以在这种情况下,要么我们可以定义一个浮点数的变量,并在传参的时候传递这个变量过去,要么就是强制转换,即
printf(“%s”,(float)123),这种的话,输出结果是正确的。
浮点数的存储方式并不像整形那样,网上的介绍很多,不懂的可以去看看,如果以后有机会,我会将其记录下来。