自己写的仿fprintf()函数和可变参数函数浅析

自己写的仿fprintf()函数和可变参数函数浅析
编辑程序绝不是一件简单的事情,我也是如此。因为c语言的强大之处,所以我觉得即使是一年前学的c语言,我还是对于有些语法难以解释的清。而正因为难以解释的清,我在开始一个项目的时候本认为可以顺利的完成的,结果时间拖了又拖,最后还是没有完成。
这个周末我一直在研究怎样写一个属性脚本系统。这个脚本系统主要是实现用文本文件来控制程序的某些参数。现在还没有完成。我的思路是能够快速地通过脚本文件对程序中的参数进行赋值。结果我又考虑程序的实用性,就想到了使用可变参数函数的部分。第一个能使我想到的可变函数就是printf()函数。这个函数能将格式化的数字传送到字符串中,并且输出。这是多么的神奇啊。美好的东西总是引起我们的好奇。这不前两天我就开始着手研究printf()函数的实现机理。
对c/c++入门的人一定知道可变参数函数。这种函数的特征有二:第一是至少有一个固定参数,第二,可变参数部分总是在固定参数的后面。如我写的函数:
bool PutValue( const char* fmt, ... );
就是这样一种可变参数函数。高手们可定能够熟练地使用va_start()、va_arg()和va_end()宏了,因为他们是可变参数的“三剑客”。有了它们,稍微有些c语言知识就能够编出高效的可变参数函数了。但我还是不知足,我要了解这些宏的实现原理。现在把stdarg.h中的相关定义展示给大家。

Code:
  1. typedefchar*va_list;
  2. #define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
  3. #defineva_start(ap,v)(ap=(va_list)&v+_INTSIZEOF(v))
  4. #defineva_arg(ap,t)(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))
  5. #defineva_end(ap)(ap=(va_list)0)

从中看出,这些宏有些生涩难懂。我都是在复习了c语言位运算和原码、补码知识后才看得懂的。如果大家感兴趣的话,可以看看相关的知识。
然后我们知道,使用windows操作系统和intel的CPU,栈是自顶向下的。也就是说栈顶在低位地址,栈底在高位地址。在传递参数的时候,通过调用默认的调用规范__cdecl,参数的传递顺序是从右到左。最终的结果是最左边第一个参数处于最低地址处,最右边最后一个参数处于最高的地址处。而使用...编译器又不会报错。于是我们想可不可以通过第一个参数的地址找到所有的参数呢?答案是肯定的。
但是我在这里遇到一个难以解决的问题:对于float类型参数似乎不起作用。因为得出的不是我想要的答案。这又该如何解决呢?我在网上查到了相关的问题,原来是char、short、float得到了提升,也就是“加宽”。因为intel的CPU的栈元素统一是sizeof(int)字节长。也就是4字节。所以小于4字节的char、short都被提升至4字节了,为了访问的方便。而同样是4字节的float由于某种特殊的考量(其实我也不知道是为什么,可能是提高精度吧。),被提升至了double,也就是8字节的水平。所以在内存中float型占用的空间是8字节。
使用了VS的调试器查看了内存,并且翻了汇编语言程序设计的IEEE浮点数存储方式,基本上验证我的猜想。于是我对计算机组成原理有了全新的认识(不过我们还没有上这门课)。下面我就用一个小小的实例来演示可变参数函数的使用(使用的是VS2005编译器,XP系统):

Code:
  1. #include<iostream>
  2. usingnamespacestd;
  3. voidfun(inta,/*floatb,floatc,floatd,floate*/...)
  4. {
  5. int*temp2=&a+1;
  6. //cout<<&b<<'/n';
  7. //cout<<&c<<'/n';
  8. //cout<<&d<<'/n';
  9. //cout<<&e<<'/n';
  10. cout<<"Temp2="<<temp2<<'/n';
  11. //(double*)temp;
  12. //float*temp=(float*)temp2-1;
  13. //temp=&b,cout<<*temp<<'/n';
  14. //temp=&c,cout<<*temp<<'/n';
  15. //temp=&d,cout<<*temp<<'/n';
  16. //temp=&e,cout<<*temp<<'/n';
  17. double*temp3=(double*)temp2;
  18. for(inti=0;i<a;++i)
  19. {
  20. cout<<*temp3<<endl;
  21. temp3++;
  22. }
  23. }
  24. intmain()
  25. {
  26. floata=1.06f;
  27. floatb=77.03f;
  28. floatc=63.04f;
  29. floatd=94.05f;
  30. fun(4,a,b,c,d);
  31. //system("pause");
  32. return0;
  33. }

程序的截图如下图所示:
如果大家想深入研究的话,可以将注释去掉,这样能显示更多的内容。

那么printf()是怎么实现的呢?
大家应该猜出来了。printf()函数是靠“%”来对传入的参数个数进行统计的。这并不意味着它能够检测出你的参数个数是否和“%”个数保持一致。如果有这样一个语句:
MyPrintf( "%d,%f,%c", a, b );
且没有引入异常处理机制,那么它的后果是未知的,因为它访问了不该访问的区域。
讲到这里我应该把自己的仿fprintf()函数给大家展示一下了。这个函数有些长,主要是判断类型用了不少语句,但是这个程序能基本与printf()函数的格式说明一致,我只好这么做了。下面我说明一下各个格式标识符的意思:

%c 字符型(char
%sd
有符号短整型(signed short
%s
字符串型(char*)(未实现)
%us
无符号短整型(unsigned short
%ui
无符号整型(unsigned int
%ud
无符号整型(unsigned int
%ul
无符号长整型(unsigned long
%i
有符号整型(signed int
%d
有符号整型(signed int
%f
浮点型(float
%lf
双精度浮点型(double
%ld
有符号长整型(signed long
%li
有符号长整型(signed long

上述格式标识符使用大写字母也有效。
函数名命名为PutValue(),以下就是我这个函数的实现:

Code:
  1. boolJPropertyScript::PutValue(constchar*fmt,...)
  2. {
  3. assert(fmt!=0);//实现断言,防止错误引起的崩溃
  4. void*pBase=(char*)(&fmt)+sizeof(char*);
  5. inti,j,k,varNum=0;
  6. intlength=int(strlen(fmt));
  7. //第一次遍历格式字符串,得到变量的数量
  8. for(i=0;i<length;i++)if(fmt[i]=='%')varNum++;
  9. char**ppVarName=newchar*[varNum];//开辟空间,用来存入文件
  10. assert(ppVarName!=0);//实现断言,防止错误引起的崩溃
  11. for(i=0;i<varNum;i++)
  12. {
  13. ppVarName[i]=newchar[64];
  14. assert(ppVarName[i]!=0);//实现断言,防止错误引起的崩溃
  15. memset(ppVarName[i],0,64);//清零
  16. }
  17. ofstreamwrite;
  18. write.open(fileName,std::ios::out);
  19. //第二次遍历格式字符串,存入文件
  20. for(i=j=k=0;i<varNum;j++)
  21. {
  22. if(fmt[j]=='%')
  23. {
  24. write<<ppVarName[i];
  25. switch(fmt[j+1])
  26. {
  27. case'c':case'C':
  28. {
  29. signedint*p=(signedint*)pBase;
  30. write<<*p<<'/n';
  31. j++,p++;
  32. pBase=(void*)p;
  33. break;
  34. }
  35. case's':case'S':
  36. if(fmt[j+2]=='d')
  37. {
  38. signedint*p=(signedint*)pBase;
  39. write<<*p<<'/n';
  40. j+=2,p++;
  41. pBase=(void*)p;
  42. }
  43. else/*Dosomethingaboutthestring*/;
  44. break;
  45. case'u':case'U':
  46. switch(fmt[j+2])
  47. {
  48. case's':case'S':
  49. {
  50. unsignedint*p=(unsignedint*)pBase;
  51. write<<*p<<'/n';
  52. j+=2,p++;
  53. pBase=(void*)p;
  54. break;
  55. }
  56. case'i':case'I':
  57. case'd':case'D':
  58. {
  59. unsignedint*p=(unsignedint*)pBase;
  60. write<<*p<<'/n';
  61. j+=2,p++;
  62. pBase=(void*)p;
  63. break;
  64. }
  65. case'l':case'L':
  66. {
  67. unsignedlong*p=(unsignedlong*)pBase;
  68. write<<*p<<'/n';
  69. j+=2,p++;
  70. pBase=(void*)p;
  71. break;
  72. }
  73. }
  74. case'i':case'I':
  75. case'd':case'D':
  76. {
  77. signedint*p=(signedint*)pBase;
  78. write<<*p<<'/n';
  79. j++,p++;
  80. pBase=(void*)p;
  81. break;
  82. }
  83. case'f':case'F':
  84. {
  85. double*p=(double*)pBase;
  86. write<<*p<<'/n';
  87. j++,p++;
  88. pBase=(void*)p;
  89. break;
  90. }
  91. case'l':case'L':
  92. switch(fmt[j+2])
  93. {
  94. case'f':case'F':
  95. {
  96. double*p=(double*)pBase;
  97. write<<*p<<'/n';
  98. j+=2,p++;
  99. pBase=(void*)p;
  100. break;
  101. }
  102. case'd':case'D':
  103. case'i':case'I':
  104. signedlong*p=(signedlong*)pBase;
  105. write<<*p<<'/n';
  106. j+=2,p++;
  107. pBase=(void*)p;
  108. break;
  109. }
  110. }
  111. i++,k=0;
  112. }
  113. elseppVarName[i][k]=fmt[j],k++;
  114. }
  115. write.close();
  116. for(i=0;i<varNum;i++)
  117. delete[]ppVarName[i];
  118. delete[]ppVarName;
  119. returntrue;
  120. }
  121. /*--------------------------------------------------------------------------*/

这里fileName涉及到一个类的私有成员,且与主题无关,因此略去。
主函数使用这条语句进行调用:
temp.PutValue( "好东西=%f这样的=%f", 12.1f, 12.4f )
其中temp是一个类的对象,与主题无关在此略去。
打开我们创建的fileName,结果我们可以看到如下文本:

好东西=12.1
这样的=12.4

这说明我们将float提升(加宽)成了double,再写入文件结果成功了。

参考文献:

从printf谈可变参数函数的实现 戎亚新 pdf文件

C语言函数入栈顺序与可变参数函数 http://apps.hi.baidu.com/share/detail/18887015
亲密接触C可变参数函数 http://blog.csdn.net/linyt/archive/2008/04/02/2243605.aspx
可变参数列表函数,参数为float类型时会读入错误以及解决方法 http://blog.csdn.net/douyangyang/archive/2009/04/01/4041768.aspx
在可变参数的函数里,为什么float型要提升为Double? http://topic.csdn.net/u/20091103/16/f2532c66-d24b-4e73-85df-c9e1d9ef5c75.html
C语言中的可变参数函数“…” http://zwh50687695.blog.163.com/blog/static/22311633201061895932503/
关于不定参数[转] http://hi.baidu.com/mgqw/blog/item/9b7a52a2ffbbecabcaefd040.html
可变参数的问题 http://blog.csdn.net/stormlk1983/archive/2010/03/05/5345153.aspx
switch语句中case跳过变量初始化的问题 http://hi.baidu.com/lovebirds/blog/item/b3de71f4762195def3d385ab.html

今天就说到这里了,有时间的话我还会完成我的属性脚本系统的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值