写点什么

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

作者:桑榆
  • 2022-10-15
    广东
  • 本文字数:4737 字

    阅读完需:约 1 分钟

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

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

函数逻辑分析---vfprintf

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

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

  • 入参

    Ch:待输出的字符

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

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

    一个是保证给到 PUTC 的入参一定是合法的 int 型数据;

    另一个是保证形如 outchar (*w++)这样的调用在宏展开时不会遇到符号优先级变化的问题。

      这种情况*w++就会被直接展开,可能在后续的使用中导致出现符号异常(++运算符被解释为对后面的变量++),所以我们有两种解决方式,一个是保证这里传给 PUTC 的已经是一个计算出来的结果,一个是在 PUTC 中加括号保证有限级。在 Glibc 的宏的使用中我们会看到很多宏中添加的括号,也是为了保证这一点。

  2. 做了异常判断:PUTC (outc, s) == EOF 表示输出字符失败,done == INT_MAX 表示当前已经到达最大输出字符数,这种情况会失败;

  3. 如果输出成功,那么我们就将 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. 检查是否已经到达最大输出字符数量;

  2. 调用前面提到的 PUT 宏输出指定长度的字符串,如果返回值不等于指定长度,则认为函数调用失败,返回-1;

  3. 如果调用 PUT 宏成功,那么需要调用 done_add_func 函数计算当前 done 的值,并进行返回。

    done = outstring_func (s, (const UCHAR_T *) buf, written, done);    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);}
复制代码

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

  • 入参

    String:buffer 地址

    Len:输出字符串长度

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

  1. 对 String 的重新赋值:const void *string_ = (String),原因与之前说到的类似,保证宏展开之后可以正常运算;

  2. 函数传参时对 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 intoutstring_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(计数总需要输出的字符数);

  2. 如果 prec 小于 0,说明不进行字符输出,但是我们仍要计算 total_written,注意,对应的__wcsrtombs 函数中有专门针对 dst 为空指针的情况,这里我们就不进行展开了。

  3. 如果 prec 非负,说明需要进行字符转换后进行计算:

      注意:因为源字符不能够以 nul 结尾,所以这里我们需要手动对字符长度进行调整

      src_copy 每次调用 CONVERT_FROM_OTHER_STRING 之后都会被移到未转换的第一个字节位置;

      limit 表示需要转换输出的字节数;所以遍历条件就是 limit > 0 && src_copy != NULL。

    在循环中我们每次最多转换 buf_length 个字节,赋值给 write_limit,如果 write_limit > limit,说明当前需要转换的字符数没有 buf_length 个字节那么多,我们就需要更新 write_limit 为剩余的字符数;

    然后调用 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.csize_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 函数进行输出,关注以下几点:


只有上面一步计算的 total_written<width,即要输出的总字节数小于对齐宽度时才需要添加对齐字符;


对齐字符的数量为 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;}
复制代码


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

桑榆

关注

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

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

评论

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