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 int
typedef 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_t
typedef 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 int
outstring_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_fwide
int
_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 int
pad_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 16
static 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)
复制代码
评论