cstdio 中的格式化输入输出函数
fprintf 函数的实现 vfprintf 中包含了相当多的宏定义和辅助函数,接下来我们一起来分析一下它们对应的源码实现。
函数逻辑分析---vfprintf
2.宏定义/辅助函数分析
(7).一些 char 与 wchar 区分的变量
#ifndef COMPILE_WPRINTF# define vfprintf __vfprintf_internal# define CHAR_T char# define OTHER_CHAR_T wchar_t# define UCHAR_T unsigned char# define INT_T inttypedef const char *THOUSANDS_SEP_T;# define L_(Str) Str# define ISDIGIT(Ch) ((unsigned int) ((Ch) - '0') < 10)# define STR_LEN(Str) strlen (Str)
# define PUT(F, S, N) _IO_sputn ((F), (S), (N))# define PUTC(C, F) _IO_putc_unlocked (C, F)# define ORIENT if (_IO_vtable_offset (s) == 0 && _IO_fwide (s, -1) != -1)\ return -1# define CONVERT_FROM_OTHER_STRING __wcsrtombs#else# define vfprintf __vfwprintf_internal# define CHAR_T wchar_t# define OTHER_CHAR_T char/* This is a hack!!! There should be a type uwchar_t. */# define UCHAR_T unsigned int /* uwchar_t */# define INT_T wint_ttypedef wchar_t THOUSANDS_SEP_T;# define L_(Str) L##Str# define ISDIGIT(Ch) ((unsigned int) ((Ch) - L'0') < 10)# define STR_LEN(Str) __wcslen (Str)
# include <_itowa.h>
# define PUT(F, S, N) _IO_sputn ((F), (S), (N))# define PUTC(C, F) _IO_putwc_unlocked (C, F)# define ORIENT if (_IO_fwide (s, 1) != 1) return -1# define CONVERT_FROM_OTHER_STRING __mbsrtowcs
# undef _itoa# define _itoa(Val, Buf, Base, Case) _itowa (Val, Buf, Base, Case)# define _itoa_word(Val, Buf, Base, Case) _itowa_word (Val, Buf, Base, Case)# undef EOF# define EOF WEOF#endif
复制代码
①.vfprintf 的别名
标准字符---__vfprintf_internal,宽字符---__vfwprintf_internal
②.CHAR_T/OTHER_CHAR_T/UCHAR_T/INT_T 类型
标准字符:分别为 char,wchar_t,unsigned char,int
宽字符:分别为 wchar_t,char,unsigned int,wint_t
注意,这里有两个很有意思的点:
标准字符与宽字符的 OTHER_CHAR_T 都为对方;
在注释中说明了宽字符的 UCHAR_T 应该使用 uwchar_t,但这里依旧使用了 unsigned int,可能会导致异常,由于历史遗留原因,暂时并未进行修改,glibc 中的这份代码也是来自别处,与 2018 年进行的一次替换操作,commitID 698fb75b9ff5ae454a1344b5f9fafa0ca367c555
③.THOUSANDS_SEP_T---按千位进行数字划分时使用的符号
通常在西方的数字表示中,按千进行数字划分,中间通常用空格或者逗号隔开
标准字符:typedef const char *THOUSANDS_SEP_T;
宽字符:typedef wchar_t THOUSANDS_SEP_T;
④.L_(Str)---L 修饰字符串
在 C/C++中,使用 L 修饰字符串是为了将 ANSI 字符串转换成 unicode 的字符串,其中每个字符占用两个字节,如:
strlen("asd") = 3;strlen(L"asd") = 6;
复制代码
所以,这里为了兼容标准字符和宽字符,在后续使用字符串常量时都统一使用 L_("a")这样来表示。
普通字符:# define L_(Str) Str,实际上使用的还是字符本身
宽字符:# define L_(Str) L##Str,使用了"##"进行了宏拼接,在字符前加上了 L 修饰,即 L"a"。
⑤.ISDIGIT(Ch)---判断字符是否是数字
标准字符:# define ISDIGIT(Ch) ((unsigned int) ((Ch) - '0') < 10
宽字符:# define ISDIGIT(Ch) ((unsigned int) ((Ch) - L'0') < 10)
两者的调用逻辑都一致,利用了 ASCLL 码的排列顺序,用当前字符的值减去'0'的值,看是否在 10 以内(注意,宽字符使用的是 L'0' )
⑥.STR_LEN(Str)---计算字符串长度
分别调用标准字符和宽字符的字符串长度计算函数
# define STR_LEN(Str) strlen (Str)# define STR_LEN(Str) __wcslen (Str)
复制代码
⑦.PUT(F, S, N)---向 F 文件流对象中写入 N 个字符,S 是开始指针
这个宏,标准字符与宽字符并无区别。
入参
F:文件流对象
S:需要写入的字符串
N:写入的字符数量
出参:返回 EOF 或真正写入的字符数量
可以看到函数调用过程:
PUT-->_IO_sputn-->_IO_XSPUTN-->__xsputn,这里我们就不展开__xsputn 的实现方式了,另外开文进行学习分析。
static inline intoutstring_func (FILE *s, const UCHAR_T *string, size_t length, int done){ assert ((size_t) done <= (size_t) INT_MAX); if ((size_t) PUT (s, string, length) != (size_t) (length)) return -1; return done_add_func (length, done);}
# define PUT(F, S, N) _IO_sputn ((F), (S), (N))
// glibc/libio/libioP.h#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
/* The 'xsputn' hook writes upto N characters from buffer DATA. Returns EOF or the number of character actually written. It matches the streambuf::xsputn virtual function. */typedef size_t (*_IO_xsputn_t) (FILE *FP, const void *DATA, size_t N); #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N) #define _IO_WXSPUTN(FP, DATA, N) WJUMP2 (__xsputn, FP, DATA, N)
复制代码
⑧.PUTC(C, F)---向 F 文件流对象中写入 1 个字符 C
注意:这里标准字符和宽字符调用的函数不一样,我们分析标准字符的流程即可。
因为只需要写入一个字符,所以逻辑也是相对比较简单的,调用逻辑如下,在__putc_unlocked_body 宏中,我们首先检查写指针是否到了末尾,如果是的,那说明我们需要先将 buffer 写回物理文件之后再将当前的字符写到 buffer 中,这里调用了__overflow 函数,主要完成的是 flush buffer 到物理文件然后再将字符写到 buffer 中的工作,就不展开说明了;
另一种情况则比较简单,还有足够的空间,那我们只需要给当前_IO_write_ptr 指针指向的位置赋值为要写入的字符即可,别忘记对_IO_write_ptr 做递增操作。
# define PUTC(C, F) _IO_putc_unlocked (C, F)
# define PUTC(C, F) _IO_putwc_unlocked (C, F)
// glibc/libio/libio.h#define _IO_putc_unlocked(_ch, _fp) __putc_unlocked_body (_ch, _fp)
// glibc/libio/bits/types/struct_FILE.h#define __putc_unlocked_body(_ch, _fp) \ (__glibc_unlikely ((_fp)->_IO_write_ptr >= (_fp)->_IO_write_end) \ ? __overflow (_fp, (unsigned char) (_ch)) \ : (unsigned char) (*(_fp)->_IO_write_ptr++ = (_ch)))
复制代码
⑨ORIENT---检查文件流的_mode 信息
可以看到标准字符与宽字符的实现方式不一样,先看标准字符
首先检查_vtable_offset 是否等于 0(这个值只有宽字符时被赋值),然后调用_IO_fwide 函数;
_IO_fwide 函数的作用是当输入 mode 为 0 或 fp->_mode 不为 0(即之前已经被设置值)时返回 fp->_mode,否则根据传入的 mode 值修改当前的 fp->_mode 值,只有当传入 mode 值大于 0 才修改,小于 0 直接返回原值。
所以现在再看_IO_fwide (s, -1) != -1,说明是只有 fp->_mode=0,才有可能返回-1,即要求标准字符的_mode 为 0,否则就返回-1(EOF);
再看宽字符的_IO_fwide (s, 1) != 1,说明只有 fp->_mode=1 或者进行 mode 修改只有为 1,才返回 1,即要求宽字符的_mode 为 1(不管是之前设置为 1,还是当前修改为 1 的),如果没有返回 1,只有可能原来被设置为除 0,1 外的其他值,这是异常情况,返回-1(EOF)。
# define ORIENT if (_IO_vtable_offset (s) == 0 && _IO_fwide (s, -1) != -1)\ return -1 # define ORIENT if (_IO_fwide (s, 1) != 1) return -1
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
// glibc/libio/iofwide.c/* Return orientation of stream. If mode is nonzero try to change the orientation first. */#undef _IO_fwideint_IO_fwide (FILE *fp, int mode){... /* The orientation already has been determined. */ if (fp->_mode != 0 /* Or the caller simply wants to know about the current orientation. */ || mode == 0) return fp->_mode; /* Set the orientation appropriately. */ if (mode > 0) { ... }
/* Set the mode now. */ fp->_mode = mode;
return mode;}
复制代码
⑩.CONVERT_FROM_OTHER_STRING---字符转换
标准字符与宽字符的转换函数是转换到对方的函数,就不展开了
# define CONVERT_FROM_OTHER_STRING __wcsrtombs
# define CONVERT_FROM_OTHER_STRING __mbsrtowcs
复制代码
附加:宽字符额外设置的函数
如下:宽字符额外将自己的 int 型转 char 函数定义为宽字符类型(Val 是 int 型数,Buf 是输出 buffer 地址,Base 是进制信息,Case 是标识使用大写还是小写字母表示大于 10 进制的位)
将 EOF 取了别名 WEOF
# undef _itoa# define _itoa(Val, Buf, Base, Case) _itowa (Val, Buf, Base, Case)# define _itoa_word(Val, Buf, Base, Case) _itowa_word (Val, Buf, Base, Case)# undef EOF# define EOF WEOF
复制代码
(8).pad_func---对齐函数
入参
FILE *s:文件流对象
CHAR_T padchar:对齐使用的字符
int width:对齐宽度
int done:当前已经输出的字符数
逻辑也相对比较简单:
如果对齐宽度小于等于 0,说明不需要对齐,直接返回 done 即可;
如果对齐宽度大于 0,需要对齐,那就根据标准字符与宽字符的区别,选择调用_IO_padn 或_IO_wpadn
这里以_IO_padn 为例,实际上做的事情就是先构造 padbuf,这里有两个默认值,一个是 blanks,一个是 zeroes,如果不是那就需要进行拷贝;
然后调用_IO_sputn 将 padbuf 写入文件流对象中,这里看上去写了两次,实际上是针对这样一种情况,我们默认每次只写入 16 个对齐字符,先整 16 个 16 个写入,直到写入数量小于 16,这时我们写入剩余的字符数量,如果本次对齐小于 16 字符,那么会直接调用第二个_IO_sputn 逻辑处理。
最后返回真正写入的字符数
/* Output initial padding. */ if (total_written < width) { done = pad_func (s, L_(' '), width - total_written, done); if (done < 0) return done; } } static inline intpad_func (FILE *s, CHAR_T padchar, int width, int done){ if (width > 0) { ssize_t written;#ifndef COMPILE_WPRINTF written = _IO_padn (s, padchar, width);#else written = _IO_wpadn (s, padchar, width);#endif if (__glibc_unlikely (written != width)) return -1; return done_add_func (width, done); } return done;}
// glibc/libio/iopadn.c#define PADSIZE 16static char const blanks[PADSIZE] ={' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' '};static char const zeroes[PADSIZE] ={'0','0','0','0','0','0','0','0','0','0','0','0','0','0','0','0'};
ssize_t_IO_padn (FILE *fp, int pad, ssize_t count){ char padbuf[PADSIZE]; const char *padptr; int i; size_t written = 0; size_t w;
if (pad == ' ') padptr = blanks; else if (pad == '0') padptr = zeroes; else { for (i = PADSIZE; --i >= 0; ) padbuf[i] = pad; padptr = padbuf; } for (i = count; i >= PADSIZE; i -= PADSIZE) { w = _IO_sputn (fp, padptr, PADSIZE); written += w; if (w != PADSIZE) return written; }
if (i > 0) { w = _IO_sputn (fp, padptr, i); written += w; } return written;}
复制代码
(9).PAD(Padchar)---使用指定字符进行对齐操作
逻辑基本是调用 pad_func 完成的,不多赘述;这个宏主要要做了一个判断操作:
如果返回的 done(当前输出的字符总数)小于 0,那就跳转到 all_done,退出函数。
#define PAD(Padchar) \ do \ { \ done = pad_func (s, (Padchar), width, done); \ if (done < 0) \ goto all_done; \ } \ while (0)
复制代码
评论