自己动手写C语言格式化输出函数(一)

    printf系列函数,包括fprintf、sprintf函数等,其功能是将C语言的所有基本数据类型按用户要求进行格式化输出。

    printf函数几乎是所有学习C语言的人接触到的第一个函数,是C语言标准中使用频率最高的函数。

    printf函数是C语言标准函数中最著名的可变参数函数,看见printf这个函数名,就想起了C语言的说法一点也不过分,因此,可以说是C语言标准函数中的最具标志性的函数。

    printf系列函数。在DOS环境下,这一系列输出函数涵盖了PC机所能用到的所有输出设备,所以printf系列函数也是C语言中最复杂的函数。

    当然,随着DOS时代的结束,不仅printf系列函数的作用减弱了,就连C语言本身也被压缩到了最小的应用领域。 

    本文写的sprintfA函数,也是应一个小友要求写的几个函数之一,包括我昨天发布的《自己动手写C语言浮点数转换字符串函数》中的FloatToStr函数,是用来学习用的。之所以取名为sprintfA,不仅是区别系统本身的sprintf函数,同时也因为在Windows下,A表示的是传统的ANSI函数。因为在Windows下,printf系列函数也“与时俱进”了,如wprintf等就是在宽字符环境下的输出函数。由于我在sprintfA函数中使用了Windows的宽字符转换函数,因此该函数只适用于Windows环境。

    由于sprintfA函数代码比较长,将分为多篇文章发布,《自己动手写C语言浮点数转换字符串函数》 一文中的代码也应算作一篇:

    一、数据定义:

 1 typedef  struct
 2 {
 3     INT type;         //  数据长度类型
 4      INT width;         //  数据最小宽度
 5      INT precision;     //  数据精度
 6      BOOL left;         //  是否居左
 7      BOOL zero;         //  是否前导零
 8      INT decimals;     //  浮点数: 1强制小数位; 16进制: -1: 0x, 1: 0X
 9      INT negative;     //  符号:-1: '-'; 1: '+'
10      LPSTR param;     //  参数指针
11  }FormatRec;
12 
13 typedef     long  long            LLONG, *PLLONG;
14 typedef    unsigned  long  long    ULLONG, *PULLONG;
15 
16  #define    TYPE_CHAR        0
17  #define    TYPE_SHORT        1
18  #define    TYPE_GENERAL    2
19  #define    TYPE_LONG        3
20  #define    TYPE_LLONG        4
21 
22  #define    PTR_SIZE        sizeof(VOID*)
23  #define    TypeSize(size)    (((size + PTR_SIZE - 1) / PTR_SIZE) * PTR_SIZE)
24 
25  #define    TS_PTR            PTR_SIZE
26  #define    TS_CHAR            TypeSize(sizeof(CHAR))
27  #define    TS_WCHAR        TypeSize(sizeof(WCHAR))
28  #define    TS_SHORT        TypeSize(sizeof(SHORT))
29  #define    TS_INT            TypeSize(sizeof(INT))
30  #define    TS_LONG            TypeSize(sizeof(LONG))
31  #define    TS_LLONG        TypeSize(sizeof(LLONG))
32  #define    TS_FLOAT        TypeSize(sizeof(FLOAT))
33  #define    TS_DOUBLE        TypeSize(sizeof(double))
34  #define    TS_EXTENDED        TypeSize(sizeof(EXTENDED))
35 
36  #define    CHAR_SPACE        ' '
37  #define    CHAR_ZERO        '0'
38  #define    CHAR_POS        '+'
39  #define    CHAR_NEG        '-'
40 
41  #define    HEX_PREFIX_U    "0X"
42  #define    HEX_PREFIX_L    "0x"
43 
44  #define    MAX_DIGITS_SIZE    40

    TYPE_XXXX是数据类型标记,对应于FormatRec.type字段。

    TS_XXXX是各种数据类型在sprintfA可变参数传递时所占的栈字节长度。除指针类型和INT类型长度直接用sizeof关键字确定栈字节长度外,其它数据类型所占栈长度则用TypeSize宏配合计算取得,这样就使得这些数据所占栈字节长度在各种环境下都是正确的,比如字符型长度为1字节,TypeSizesizeof(CHAR)),在32位编译环境时等于4,在64位编译环境时则等于8。

   对于带任意类型可变参数的函数来说,实参数据类型的栈字节长度正确与否,完全取决于程序员。比如在sprintfA的格式参数中定义了%Ld,应该是个64位整数类型,而在对应的可变参数部分却给了一个int类型,在32位编译器环境下,就存在2个错误,一是数据类型不正确,二是栈字节长度不匹配,64位整数长度为8字节,而INT的长度却只有4字节,其结果就是这个数据以及其后的所有数据都会出现错误的显示结果,甚至还有可能造成程序崩溃。这也是一些C语言初学者在使用printf系列函数时最容易犯的错误,他们混淆了一般函数与带可变参数函数调用的区别, 对于一般的C函数,形参的数据类型是固定的,在调用时,如果实参与形参数据类型不匹配,编译器视情况会作出错误、警告或者转换等处理,而对于不同精度的相同数据类型,编译器大都会自动进行扩展或截断;而调用带可变参数函数时,由于函数原型的形参说明部分为“...”,编译器就没法将int扩展为_int64了。

    另外,还有有关浮点数部分的数据定义在《自己动手写C语言浮点数转换字符串函数》 。

    二、函数主体。

 

  1  //  获取字符串中的数字。参数:字符串,数字指针。返回字符串中最后一个数字位置
  2  static LPSTR GetControlNum(LPCSTR s, INT *value)
  3 {
  4     register LPCSTR p = s;
  5     register INT v;
  6      for (v =  0; *p >=  ' 0 ' && *p <=  ' 9 '; p ++)
  7         v = v *  10 + (*p -  ' 0 ');
  8     *value = v;
  9      return (LPSTR)(p -  1);
 10 }
 11 
 12 LPSTR _sprintfA(LPSTR buffer, LPCSTR format, ...)
 13 {
 14     FormatRec rec;
 15     BOOL flag;
 16     CHAR c;
 17     LPCSTR psave;             //  ?
 18      register LPCSTR pf = format;
 19     register LPSTR pb = buffer;
 20     va_list paramList;
 21 
 22     va_start(paramList, format);
 23     rec.param = (LPSTR)paramList;
 24      while (TRUE)
 25     {
 26          while (*pf && *pf !=  ' % ')
 27             *pb ++ = *pf ++;
 28          if (*pf ==  0break;
 29          if (*(pf +  1) ==  ' % ')     //  处理%%
 30          {
 31             *pb ++ =  ' % ';
 32             pf +=  2;
 33              continue;
 34         }
 35         psave = pf;             //  ?
 36          rec.width = rec.decimals = rec.negative =  0;
 37         rec.left = rec.zero = FALSE;
 38         rec.type = TYPE_GENERAL;
 39         rec.precision = - 1;
 40          //  解析前导符号
 41          flag = TRUE;
 42          while (flag)
 43         {
 44             pf ++;
 45              switch (*pf)
 46             {
 47                  case  ' 0 ':
 48                     rec.zero = TRUE;
 49                     flag = FALSE;
 50                      break;
 51                  case  ' - ':
 52                     rec.left = TRUE;
 53                      break;
 54                  case  ' + ':
 55                     rec.negative =  1;
 56                      break;
 57                  case  ' # ':
 58                     rec.decimals =  1;
 59                      break;
 60                  default:
 61                     pf --;
 62                     flag = FALSE;
 63                      break;
 64             }
 65         }
 66          //  解析输出宽度和精度
 67          flag = TRUE;
 68          while (flag)
 69         {
 70             pf ++;
 71              switch (*pf)
 72             {
 73                  case  ' . ':         //  如小数点后为'*','0' - '9'继续处理精度和宽度
 74                      rec.precision =  0;
 75                     c = *(pf +  1);
 76                     flag = (c ==  ' * ' || (c >=  ' 0 ' && c <=  ' 9 '));
 77                      break;
 78                  case  ' * ':         //  处理'*'表示的宽度参数和精度参数
 79                       if (*(pf -  1) ==  ' . ')
 80                     {
 81                         rec.precision = *(PINT)rec.param;
 82                         flag = FALSE;
 83                     }
 84                      else
 85                     {
 86                         rec.width = *(PINT)rec.param;
 87                         flag = *(pf +  1) ==  ' . ';
 88                     }
 89                     rec.param += TS_PTR;
 90                      break;
 91                  default:         //  处理格式串中数字表示的宽度和精度
 92                       if (*(pf -  1) ==  ' . ')
 93                     {
 94                         pf = GetControlNum(pf, &rec.precision);
 95                         flag = FALSE;
 96                     }
 97                      else
 98                     {
 99                         pf = GetControlNum(pf, &rec.width);
100                         flag = *(pf +  1) ==  ' . ';
101                     }
102             }
103         }
104          //  解析数据类型精度
105          flag = TRUE;
106          while (flag)
107         {
108             pf ++;
109              switch(*pf)
110             {
111                  case  ' L ':
112                     rec.type = TYPE_LLONG;
113                      break;
114                  case  ' l ':
115                      if (rec.type < TYPE_LLONG)
116                         rec.type ++;
117                      break;
118                  case  ' H ':
119                     rec.type = TYPE_CHAR;
120                      break;
121                  case  ' h ':
122                      if (rec.type > TYPE_CHAR)
123                         rec.type --;
124                      break;
125                  default:
126                     flag = FALSE;
127             }
128         }
129          //  解析数据类型,并格式化
130          c = *pf ++;
131          switch (c)
132         {
133              case  ' s ':
134                 pb = FormatStrA(pb, &rec);
135                  break;
136              case  ' c ':
137                 pb = FormatCharA(pb, &rec);
138                  break;
139              case  ' d ':
140              case  ' i ':
141              case  ' u ':
142                 pb = FormatIntA(pb, &rec, c ==  ' u ');
143                  break;
144              case  ' f ':
145                 pb = FormatFloatFA(pb, &rec);
146                  break;
147              case  ' e ':
148              case  ' E ':
149                 pb = FormatFloatEA(pb, &rec, c);
150                  break;
151              case  ' g ':
152              case  ' G ':
153                 pb = FormatFloatGA(pb, &rec, c);
154                  break;
155              case  ' x ':
156                  if (rec.decimals)
157                     rec.decimals = - 1;
158              case  ' X ':
159                 pb = FormatHexA(pb, &rec, c);
160                  break;
161              case  ' o ':
162                 pb = FormatOctalA(pb, &rec);
163                  break;
164              case  ' p ':
165                 pb = FormatPointerA(pb, &rec);
166                  break;
167              case  ' n ':
168                 GetPosSizeA(pb, buffer, &rec);
169                  break;
170              default:             //  错误:拷贝format剩余字符,返回
171  //                 pf = psave + 1;     //  ? 也可处理为忽略后继续
172  //                 break;             //  ?
173                  lstrcpyA(pb, psave);
174                  return buffer;
175         }
176     }
177     va_end(paramList);
178     *pb =  0;
179      return buffer;
180 }

 

     sprintfA函数的主体部分就是一个简单的解释器,通过一个主循环,对字符串参数format逐字符的作如下解析:

    1)如果不是数据格式前缀字符'%',直接拷贝到输出缓冲区buffer;

    2)如果'%'后接着一个'%'字符,则表示要输出后面这个'%';

    3)紧接着'%'后面的,应该是数据格式前导字符。共有4个前导字符:

        1、'0':前导零标志。如果数据被格式化后的长度小于规定的格式化宽度,则在被格式化后的数据前补0;

        2、'-':左对齐标记。

        3、'+':正数符号输出标记。正数在正常格式输出时,其符号是省略了的,'+'则表示要输出这个符号;

        4、'#':对浮点数,这是强制小数点('.')输出标记。无论这个数有没有小数部分,都必须输出这个小数位符号;对整数的十六进制输出,则是十六进制前缀(0x或者0X)输出标记。

    前导字符不是必须的,也可有多个前导符同时出现在'%'后面,但'0'必须排在最后一个,其余顺序可任意。

    4)解析数据输出宽度和精度。宽度是指数据输出时必须达到的字节数,如果格式化后的数据长度小于宽度,应用空格或者零补齐;精度则是数据要求格式化的长度,视数据类型不同而有所区别,如浮点数是指小数部分的长度,而其它数据则是指全部数据格式化长度,大于精度的数据是保留还是截断,小于精度是忽略还是补齐(零或空格),后面涉及具体数据类型时再说明。

    宽度和精度一般以'.'为分隔符,左边是宽度,右边是精度,如果只有宽度则'.'可忽略。宽度和精度可用固定数字表示,如“10.6”,也可用可变形式“*.*”表示。可变形式的宽度和精度必须在sprintf的可变参数部分有其对应的整数实参。

    宽度和精度部分也不是必须的。

    5)分析数据类型精度字符。在C语言中,相同类型的基本数据可能有不同的精度,如整数有长短之分,浮点数有精度之分,而字符有ANSI和UNICODE之分等等。在sprintfA中,是靠分析类型精度字符来取得的。字符'l'和'h'分别表示长数据和短数据,在16位编译器环境下,一个'l'或'h'就够了,而32位及以上编译器中,随着数据精度的提高,必须靠多个类型精度字符才能表示完整,为此,也可用字符'L'和'H'分别表示数据类型的最大精度和最小精度。sprintfA的数据类型精度分析有较高的容错处理,你可以输入任意多个类型精度字符。

    类型精度字符也不是必须的,缺省情况下,按一般类型精度处理。

    6)解析数据类型字符。数据类型字符的作用有2个,一是确定将要输出的数据类型,如x是整型数,e是浮点数等;二是确定要输出的形式,x是以小写十六进制输出整型数,e则是以指数形式输出浮点数。

    数据类型字符是必须的。数据类型字符解析完毕,各种信息写入FormatRec结构,接着就是具体的各种数据的格式化过程了,其代码将在后面给出。

    7)错误处理。如果在'%'字符后,出现上述各种字符以外的字符,或者上述各种字符排列顺序错误,就需要进行错误处理。printf系列函数的错误处理在不同的编译器中的处理方式是不一样的,主要有2种处理方式:一是忽略本次数据分析,format指针退回到'%'之后,继续循环('%'后的字符作一般字符处理);二是不再作分析,直接将'%'后的所有字符输出到buffer后退出函数。本文sprintfA函数采用了后一种处理方式,前一种处理方式在函数主体中也能找到,就是被注释了的语句。

    如果没有错误,则回到1),继续下一数据分析。

    未完待续......

    

    声明:本文代码主要供学习使用,如作其它用途,出问题慨不负责。

    水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]

你可能感兴趣的:(C语言)