多参的实现原理

相信大家都使用过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);
}

本人菜鸟,水平有限,望各路大牛指点!

posted @ 2013-04-22 19:10  0x苦行僧  阅读(464)  评论(0)    收藏  举报