写点什么

cstdio 的源码学习分析 10- 格式化输入输出函数 fprintf--- 宏定义 / 辅助函数分析 02

作者:桑榆
  • 2022-10-14
    广东
  • 本文字数:5139 字

    阅读完需:约 1 分钟

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

注意,这里有两个很有意思的点:

  1. 标准字符与宽字符的 OTHER_CHAR_T 都为对方;

  2. 在注释中说明了宽字符的 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 修饰字符串

  • 入参

    Str:字符串

在 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)---判断字符是否是数字

  • 入参

    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)---计算字符串长度

  • 入参

    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

  • 入参

    C:要写入的字符

    F:文件流对象

注意:这里标准字符和宽字符调用的函数不一样,我们分析标准字符的流程即可。

因为只需要写入一个字符,所以逻辑也是相对比较简单的,调用逻辑如下,在__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:当前已经输出的字符数

逻辑也相对比较简单:

  1. 如果对齐宽度小于等于 0,说明不需要对齐,直接返回 done 即可;

  2. 如果对齐宽度大于 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)
复制代码


发布于: 刚刚阅读数: 3
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020-02-29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
cstdio的源码学习分析10-格式化输入输出函数fprintf---宏定义/辅助函数分析02_源码刨析_桑榆_InfoQ写作社区