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

190 阅读5分钟

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

cstdio中的格式化输入输出函数

fprintf函数的实现vfprintf中包含了相当多的宏定义和辅助函数,接下来我们一起来分析一下它们对应的源码实现。

函数逻辑分析---vfprintf

2.宏定义/辅助函数分析

(10).outchar(Ch)---输出字符Ch

  • 入参

    • Ch:待输出的字符

这里的核心逻辑是调用前面提到过的PUTC宏将字符Ch输出到对应的文件流s中,但是有以下几点需要注意:

  1. 对Ch的转换操作:const INT_T outc = (Ch),这个操作看上去没有什么作用,可以直接填入PUTC宏中,实际上这里有两个作用:

    1. 一个是保证给到PUTC的入参一定是合法的int型数据;
    2. 另一个是保证形如outchar (*w++)这样的调用在宏展开时不会遇到符号优先级变化的问题。
    3.   这种情况*w++就会被直接展开,可能在后续的使用中导致出现符号异常(++运算符被解释为对后面的变量++),所以我们有两种解决方式,一个是保证这里传给PUTC的已经是一个计算出来的结果,一个是在PUTC中加括号保证有限级。在Glibc的宏的使用中我们会看到很多宏中添加的括号,也是为了保证这一点。
  1. 做了异常判断:PUTC (outc, s) == EOF表示输出字符失败,done == INT_MAX表示当前已经到达最大输出字符数,这种情况会失败;
  1. 如果输出成功,那么我们就将done(计算当前输出字符数)加一。
    outchar (*w++);
    
#define outchar(Ch)                               \
  do                                          \
    {                                         \
      const INT_T outc = (Ch);                            \
      if (PUTC (outc, s) == EOF || done == INT_MAX)               \
    {                                     \
      done = -1;                                  \
      goto all_done;                              \
    }                                     \
      ++done;                                     \
    }                                         \
  while (0)

(11).outstring_func---输出字符串函数

  • 入参

    • FILE *s:文件流对象
    • const UCHAR_T *string:buffer地址
    • size_t length:输出字符串长度
    • int done:当前已输出字符数
  • 出参:返回当前已输出字符数

函数逻辑比较清晰:

  1. 检查是否已经到达最大输出字符数量;
  1. 调用前面提到的PUT宏输出指定长度的字符串,如果返回值不等于指定长度,则认为函数调用失败,返回-1;
  1. 如果调用PUT宏成功,那么需要调用done_add_func函数计算当前done的值,并进行返回。
    done = outstring_func (s, (const UCHAR_T *) buf, written, done);
    
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);
}

(12).outstring---输出字符串

  • 入参

    • String:buffer地址
    • Len:输出字符串长度

实际上是outstring_func的宏封装,做了容错机制,当返回done小于0时,直接跳转到all_done,这里有以下两点需要关注:

  1. 对String的重新赋值:const void *string_ = (String),原因与之前说到的类似,保证宏展开之后可以正常运算;
  1. 函数传参时对Len的加括号,这里我们通过例子可以看到,传参时,是可能传入一个待计算结果的,加括号,保证运算优先级。
  /* Write the literal text before the first format.  */
  outstring ((const UCHAR_T *) format,
         lead_str_end - (const UCHAR_T *) format);

#define outstring(String, Len)                      \
  do                                    \
    {                                   \
      const void *string_ = (String);                   \
      done = outstring_func (s, string_, (Len), done);          \
      if (done < 0)                         \
    goto all_done;                          \
    }                                   \
   while (0)

(13).outstring_converted_wide_string---输出宽字符函数

①.函数入口

  • 入参

    • FILE *s:文件流对象
    • const OTHER_CHAR_T *src:待输出的源字符串
    • int prec:输出的字节数
    • int width:对齐宽度
    • bool left:是否左对齐
    • int done:当前已输出字符数
  • 出参:返回当前已输出字符数

注意:这里的源字符串使用了OTHER_CHAR_T,还记得我们之前提到过,标准字符和宽字符互相把对方设置为自己的OTHER_CHAR_T,所以这里我们以OTHER_CHAR_T为wchar_t来进行分析。

/* Write the string SRC to S.  If PREC is non-negative, write at most
   PREC bytes.  If LEFT is true, perform left justification.  */
static int
outstring_converted_wide_string (FILE *s, const OTHER_CHAR_T *src, int prec,
                 int width, bool left, int done)
{

②.变量准备

因为要处理宽字符,所以我们必须要申请一块buffer来处理转换后的数据,这块buffer的大小要满足__wcsrtombs的需求,这里,我们使用了256/sizeof(CHAR_T),应该是足够使用的,同时,我们我们使用assert判断我们申请的buf大小是否满足Glibc内部规定的最大宽字符长度。

  /* Use a small buffer to combine processing of multiple characters.
     CONVERT_FROM_OTHER_STRING expects the buffer size in (wide)
     characters, and buf_length counts that.  */
  enum { buf_length = 256 / sizeof (CHAR_T) };
  CHAR_T buf[buf_length];
  _Static_assert (sizeof (buf) > MB_LEN_MAX,
          "buffer is large enough for a single multi-byte character");
          
# define CONVERT_FROM_OTHER_STRING __wcsrtombs

// glibc/include/limits.h
/* Maximum length of any multibyte character in any locale.
   We define this value here since the gcc header does not define
   the correct value.  */
#define MB_LEN_MAX  16

③.处理左边的对齐情况

如果对齐宽度大于0且没有指定左对齐,这时就需要先输出对齐字符:

首先计算上面的入参信息中一共要输出多少有效字符total_written,

然后再计算并输出对齐字符。

  /* Add the initial padding if needed.  */
  if (width > 0 && !left)                                                                                                                                
    {
    // 计算total_written长度
    ...
    // 输出对齐字符
    ...
    } 

计算total_written长度,这里的逻辑也比较清晰,主要是调用CONVERT_FROM_OTHER_STRING进行计算,流程如下:

  1. 申请局部变量mbstate(用来进行字符转换),src_copy(源字符串指针的拷贝),total_written(计数总需要输出的字符数);
  1. 如果prec小于0,说明不进行字符输出,但是我们仍要计算total_written,注意,对应的__wcsrtombs函数中有专门针对dst为空指针的情况,这里我们就不进行展开了。
  1. 如果prec非负,说明需要进行字符转换后进行计算:

    1.   注意:因为源字符不能够以nul结尾,所以这里我们需要手动对字符长度进行调整
    2.   src_copy每次调用CONVERT_FROM_OTHER_STRING之后都会被移到未转换的第一个字节位置;
    3.   limit表示需要转换输出的字节数;所以遍历条件就是limit > 0 && src_copy != NULL。
    4. 在循环中我们每次最多转换buf_length个字节,赋值给write_limit,如果write_limit > limit,说明当前需要转换的字符数没有buf_length个字节那么多,我们就需要更新write_limit为剩余的字符数;
    5. 然后调用CONVERT_FROM_OTHER_STRING进行字符转换,如果返回值为-1,说明转换失败,返回-1;如果返回值为0,说明本次没有字符需要转换,结束循环;否则就对total_written计数累加,对limit计数累减。
      /* Make a first pass to find the output width, so that we can
     add the required padding.  */                                                                                                                       
      mbstate_t mbstate = { 0 };
      const OTHER_CHAR_T *src_copy = src;
      size_t total_written;
      if (prec < 0)
    total_written = CONVERT_FROM_OTHER_STRING
      (NULL, &src_copy, 0, &mbstate);
      else
    {
      /* The source might not be null-terminated.  Enforce the
         limit manually, based on the output length.  */
      total_written = 0;
      size_t limit = prec;
      while (limit > 0 && src_copy != NULL)
        {
          size_t write_limit = buf_length;
          if (write_limit > limit)
        write_limit = limit;
          size_t written = CONVERT_FROM_OTHER_STRING
        (buf, &src_copy, write_limit, &mbstate);
          if (written == (size_t) -1)
        return -1;
          if (written == 0)
        break;
          total_written += written;
          limit -= written;
        }
    }
    
// glibc/wcsmbs/wcsrtombs.c
size_t
__wcsrtombs (char *dst, const wchar_t **src, size_t len, mbstate_t *ps)
{
...
    size_t result;
  /* We have to handle DST == NULL special.  */
  if (dst == NULL)
    {
    ....
          /* Count the number of bytes.  */
      result += data.__outbuf - buf;
      ...
    }
...
return result;
}

输出对齐字符,主要是调用pad_func函数进行输出,关注以下几点:

  1. 只有上面一步计算的total_written<width,即要输出的总字节数小于对齐宽度时才需要添加对齐字符;
  1. 对齐字符的数量为width - total_written,默认对齐字符是L_(' '),即空格。
      /* Output initial padding.  */
      if (total_written < width)
    {
      done = pad_func (s, L_(' '), width - total_written, done);
      if (done < 0)
        return done;
    }

④.字符转换与输出

这里的逻辑基本与计算total_written时基本一致,我们就不深入分析细节了。

注意,这里是调用outstring_func将转换出的buf中的数据输出到文件流对象中

 /* Convert the input string, piece by piece.  */
  size_t total_written = 0;
  {
    mbstate_t mbstate = { 0 };
    /* If prec is negative, remaining is not decremented, otherwise,
      it serves as the write limit.  */
    size_t remaining = -1;
    if (prec >= 0)
      remaining = prec;
    while (remaining > 0 && src != NULL)
      {
    size_t write_limit = buf_length;
    if (remaining < write_limit)
      write_limit = remaining;
    size_t written = CONVERT_FROM_OTHER_STRING
      (buf, &src, write_limit, &mbstate);
    if (written == (size_t) -1)
      return -1;
    if (written == 0)
      break;
    done = outstring_func (s, (const UCHAR_T *) buf, written, done);
    if (done < 0)
      return done;
    total_written += written;
    if (prec >= 0)
      remaining -= written;
      }                                                                                                                                                  
  }

⑤.处理右边的对齐情况

这里针对的是左对齐的情况,需要调用pad_func输出width - total_written个空格字符。

  /* Add final padding.  */
  if (width > 0 && left && total_written < width)
    return pad_func (s, L_(' '), width - total_written, done);

⑥.返回结果

返回当前已输出字符数

  return done;
}