现在的位置: 首页 > 综合 > 正文

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

2013年09月15日 ⁄ 综合 ⁄ 共 6681字 ⁄ 字号 评论关闭

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

Code:
  1. typedef char *va_list;    
  2.   
  3. #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )    
  4.   
  5. #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )   
  6. #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )   
  7. #define va_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. using namespace std;   
  3. void fun(int a, /*float b, float c, float d, float e*/... )    
  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 (int i = 0; i < a; ++i )   
  19.  {    
  20.   cout <<*temp3 << endl;    
  21.   temp3++;    
  22.  }    
  23. }   
  24.   
  25. int main()    
  26. {    
  27.  float a = 1.06f;    
  28.  float b = 77.03f;    
  29.  float c = 63.04f;    
  30.  float d = 94.05f;    
  31.  fun(4, a, b, c, d);    
  32.  //system("pause");    
  33.  return 0;    
  34. }  

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

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

 这里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

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

抱歉!评论已关闭.