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

130 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

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

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

  1. 标准字符与宽字符的OTHER_CHAR_T都为对方;
  1. 在注释中说明了宽字符的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 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

  • 入参

    • 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_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:当前已经输出的字符数

逻辑也相对比较简单:

  1. 如果对齐宽度小于等于0,说明不需要对齐,直接返回done即可;
  1. 如果对齐宽度大于0,需要对齐,那就根据标准字符与宽字符的区别,选择调用_IO_padn或_IO_wpadn

    1. 这里以_IO_padn为例,实际上做的事情就是先构造padbuf,这里有两个默认值,一个是blanks,一个是zeroes,如果不是那就需要进行拷贝;
    2. 然后调用_IO_sputn将padbuf写入文件流对象中,这里看上去写了两次,实际上是针对这样一种情况,我们默认每次只写入16个对齐字符,先整16个16个写入,直到写入数量小于16,这时我们写入剩余的字符数量,如果本次对齐小于16字符,那么会直接调用第二个_IO_sputn逻辑处理。
    3. 最后返回真正写入的字符数
      /* 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)