相信大家都使用过C语言的库函数:printf("%d%d", 1, 2)的吧,使用确实很方便功能也很强大。
但是为什么它可以接受多个参数呢?
现在我们来解析一下多参的实现原理,网上也找了一些文章。发现解析得都不全面。并且有BUG。
先看如下源码:
#include <windows.h> #include <stdio.h> #include <winnt.h>void MySprintf(char* szBuffer, const char* szFormat, ...) {va_list pa; // 定义一个指针va_start(pa, szFormat); // 把指针赋值为第一个参数的值vsprintf(szBuffer, szFormat, pa);// va_end(pa); // 清空 } int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) {char szBuffer[128] = {0};MySprintf(szBuffer, "%d%d%d", 1,2,3);return 0 ; }
首先,我们函数压参顺序是重右往左,对应的栈空间内存地址从高到低。
MySprintf(szBuffer, "%d%d%d", 1,2,3);
这行代码,分别想栈中压入3, 2, 1, 然后再是szFormat的内存空间,在是szBuffer的内存空间.
内存结构如下:
熟悉函数调用的几步过程,压入参数,保存返回地址和栈顶,开辟局部变量空间.
现在保存了返回地址,然后继续执行的话,就是保存栈顶和开辟va_list pa所需的内存空间.
va_list其实也就是char* 类型。占4个字节。继续执行后如下:
看到了吗,va_start(pa, szFormat); 这条语句计算出了参数的起始地址,
它是如何计算出的呢?我们既然知道了内存布局, 那szFormat取地址+sizeof(va_list)。
说简单点也就是:szFormat的内存地址 + 4个字节. 不就刚好偏移到第一个参数的地址处了吗?
并且, 以上的MySprintf函数等同于下面这种写法:
void MySprintf(char* szBuffer, const char* szFormat, ...) {char* pCh = NULL;pCh = (char*)&szFormat + sizeof(pCh);vsprintf(szBuffer, szFormat, pCh);pCh = NULL; }
现在,我们得到了参数的内存首地址,但是还缺少信息。缺什么信息?
当前地址处有几个参数,每个参数什么类型(占用字节数)?
关键的地方就在这里了。szFormat中有类型信息信息,并且有类型信息的个数,我们可以通过遍历字符串,
找出类型信息的顺序和个数。然后根据遍历找到的信息。来解析参数的内存首地址。
具体的做法.在vsprintf中有实现,下面是拷贝vsprintf的实现代码,
#ifndef _COUNT_int __cdecl vsprintf (char *string,const char *format,va_list ap) #else /* _COUNT_ */int __cdecl _vsnprintf (char *string,size_t count,const char *format,va_list ap) #endif /* _COUNT_ */{FILE str;REG1 FILE *outfile = &str;REG2 int retval;_ASSERTE(string != NULL);_ASSERTE(format != NULL);outfile->_flag = _IOWRT|_IOSTRG;outfile->_ptr = outfile->_base = string; #ifndef _COUNT_outfile->_cnt = MAXSTR; #else /* _COUNT_ */outfile->_cnt = count; #endif /* _COUNT_ *//*简单说明:关键代码处,大家直接跟进去即可,先是做些判断,然后设置一些标志位,最后把数字根据设置的标志转为字符串.*/retval = _output(outfile,format,ap );_putc_lk('\0',outfile);return(retval); }
本人菜鸟,水平有限,望各路大牛指点!