⾃⼰动⼿写C语⾔格式化输出函数(⼀)
printf系列函数,包括fprintf、sprintf函数等,其功能是将C语⾔的所有基本数据类型按⽤户要求进⾏格式化输出。
printf函数⼏乎是所有学习C语⾔的⼈接触到的第⼀个函数,是C语⾔标准中使⽤频率最⾼的函数。
printf函数是C语⾔标准函数中最著名的可变参数函数,看见printf这个函数名,就想起了C语⾔的说法⼀点也不过分,因此,可以说是C语⾔标准函数中的最具标志性的函数。
printf系列函数。在DOS环境下,这⼀系列输出函数涵盖了PC机所能⽤到的所有输出设备,所以printf系列函数也是C语⾔中最复杂的函数。
当然,随着DOS时代的结束,不仅printf系列函数的作⽤减弱了,就连C语⾔本⾝也被压缩到了最⼩的应⽤领域。
本⽂写的sprintfA函数,也是应⼀个⼩友要求写的⼏个函数之⼀,包括我昨天发布的《》中的FloatToStr函数,是⽤来学习⽤的。之所以取名为sprintfA,不仅是区别系统本⾝的sprintf函数,同时也因为在Windows下,A表⽰的是传统的ANSI函数。因为在Windows下,printf系列函数也“与时俱进”了,如wprintf 等就是在宽字符环境下的输出函数。由于我在sprintfA函数中使⽤了Windows的宽字符转换函数,因此该函数只适
⽤于Windows环境。
由于sprintfA函数代码⽐较长,将分为多篇⽂章发布,《》⼀⽂中的代码也应算作⼀篇:
⼀、数据定义:
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是数据类型标记,对应于pe字段。
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了。
另外,还有有关浮点数部分的数据定义在《》。
另外,还有有关浮点数部分的数据定义在《》。
⼆、函数主体。
1// 获取字符串中的数字。参数:字符串,数字指针。返回字符串中最后⼀个数字位置  2static LPSTR GetControlNum(LPCSTR s, INT *value)
3 {
4    register LPCSTR p = s;
5    register INT v;
6for (v = 0; *p >= '0' && *p <= '9'; p ++)
7        v = v * 10 + (*p - '0');
8    *value = v;
9return (LPSTR)(p - 1);float()函数
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;
24while (TRUE)
25    {
26while (*pf && *pf != '%')
27            *pb ++ = *pf ++;
28if (*pf == 0) break;
29if (*(pf + 1) == '%')    // 处理%%
30        {
31            *pb ++ = '%';
32            pf += 2;
33continue;
34        }
35        psave = pf;            // ?
36        rec.width = rec.decimals = ative = 0;
37        rec.left = = FALSE;
38        pe = TYPE_GENERAL;
39        rec.precision = -1;
40// 解析前导符号
41        flag = TRUE;
42while (flag)
43        {
44            pf ++;
45switch (*pf)
46            {
47case'0':
48                    = TRUE;
49                    flag = FALSE;
50break;
51case'-':
52                    rec.left = TRUE;
53break;
54case'+':
55                    ative = 1;
56break;
57case'#':
58                    rec.decimals = 1;
59break;
60default:
61                    pf --;
62                    flag = FALSE;
63break;
64            }
65        }
66// 解析输出宽度和精度
67        flag = TRUE;
68while (flag)
69        {
70            pf ++;
71switch (*pf)
72            {
73case'.':        // 如⼩数点后为'*','0' - '9'继续处理精度和宽度
74                    rec.precision = 0;
75                    c = *(pf + 1);
76                    flag = (c == '*' || (c >= '0' && c <= '9'));
77break;
78case'*':        // 处理'*'表⽰的宽度参数和精度参数
79if (*(pf - 1) == '.')
80                    {
81                        rec.precision = *(PINT)rec.param;
82                        flag = FALSE;
83                    }
83                    }
84else
85                    {
86                        rec.width = *(PINT)rec.param;
87                        flag = *(pf + 1) == '.';
88                    }
89                    rec.param += TS_PTR;
90break;
91default:        // 处理格式串中数字表⽰的宽度和精度 92if (*(pf - 1) == '.')
93                    {
94                        pf = GetControlNum(pf, &rec.precision);
95                        flag = FALSE;
96                    }
97else
98                    {
99                        pf = GetControlNum(pf, &rec.width);
100                        flag = *(pf + 1) == '.';
101                    }
102            }
103        }
104// 解析数据类型精度
105        flag = TRUE;
106while (flag)
107        {
108            pf ++;
109switch(*pf)
110            {
111case'L':
112                    pe = TYPE_LLONG;
113break;
114case'l':
115if (pe < TYPE_LLONG)
116                        pe ++;
117break;
118case'H':
119                    pe = TYPE_CHAR;
120break;
121case'h':
122if (pe > TYPE_CHAR)
123                        pe --;
124break;
125default:
126                    flag = FALSE;
127            }
128        }
129// 解析数据类型,并格式化
130        c = *pf ++;
131switch (c)
132        {
133case's':
134                pb = FormatStrA(pb, &rec);
135break;
136case'c':
137                pb = FormatCharA(pb, &rec);
138break;
139case'd':
140case'i':
141case'u':
142                pb = FormatIntA(pb, &rec, c == 'u');
143break;
144case'f':
145                pb = FormatFloatFA(pb, &rec);
146break;
147case'e':
148case'E':
149                pb = FormatFloatEA(pb, &rec, c);
150break;
151case'g':
152case'G':
153                pb = FormatFloatGA(pb, &rec, c);
154break;
155case'x':
156if (rec.decimals)
157                    rec.decimals = -1;
158case'X':
159                pb = FormatHexA(pb, &rec, c);
160break;
161case'o':
162                pb = FormatOctalA(pb, &rec);
163break;
164case'p':
165                pb = FormatPointerA(pb, &rec);
166break;
167case'n':
168                GetPosSizeA(pb, buffer, &rec);
169break;
170default:            // 错误:拷贝format剩余字符,返回171//                pf = psave + 1;    // ? 也可处理为忽略后继续172//                break;            // ?
173                lstrcpyA(pb, psave);
173                lstrcpyA(pb, psave);
174return buffer;
175        }
176    }
177    va_end(paramList);
178    *pb = 0;
179return 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),继续下⼀数据分析。
未完待续......
声明:本⽂代码主要供学习使⽤,如作其它⽤途,出问题慨不负责。
⽔平有限,错误在所难免,欢迎指正和指导。邮箱地址:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。