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

117 阅读7分钟

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

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

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

函数逻辑分析---vfprintf

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

(19)printf_positional---打印特定位置的信息

①函数入参分析

  • FILE *s:文件流对象
  • const CHAR_T *format:输入的format参数
  • int readonly_format:标识输入的format参数是否只读,1表示只读,-1表示可读可写,0表示未知
  /* 1 if format is in read-only memory, -1 if it is in writable memory,
     0 if unknown.  */
  int readonly_format = 0;
  • va_list ap:当前不定参数列表,指向下一个待解析的参数
  • va_list *ap_save:原始不定参数列表指针,指向第一个不定参数
  • int done:当前已输出的字符数
  • int nspecs_done:当前已处理的特殊类型数量,即已经处理几个占位符的打印
  • const UCHAR_T *lead_str_end:format字符串无需解析部分的末尾,要么指向format中第一个'%',要么指向'\0'
  • CHAR_T *work_buffer:局部buffer变量,保存当前生成的结果,大小为WORK_BUFFER_SIZE个CHAR_T,一般配合使用的还有CHAR_T *workend,指向work_buffer末尾的下一个元素,类似STL中的end()概念。
  • int save_errno:保存在此操作之前的errno值
  /* For the %m format we may need the current `errno' value.  */
  int save_errno = errno;

grouping---指定形成每个组的位数,对于非货币量,这些数字将由千分位分隔符分隔。 举个例子,比如thousand_sep = ',',当我们表示1000000时: grouping = '\3',那就表示为'1,000,000'; grouping = '\1\2\3',那就表示为'1,000,00,0'; grouping = '\3\1',那就表示为'1,0,0,0,000'。

thousands_sep---用于分隔非货币量小数点左侧的数字组的分隔符;

  • unsigned int mode_flags:vfprintf输入的printf打印格式
  • 返回值:done,当前输出的字符数
  /* Hand off processing for positional parameters.  */
do_positional:
  done = printf_positional (s, format, readonly_format, ap, &ap_save,                                                                                    
                done, nspecs_done, lead_str_end, work_buffer,
                save_errno, grouping, thousands_sep, mode_flags);
                
/* Handle positional format specifiers.  */
static int
printf_positional (FILE *s, const CHAR_T *format, int readonly_format,
           va_list ap, va_list *ap_savep, int done, int nspecs_done,
           const UCHAR_T *lead_str_end,
           CHAR_T *work_buffer, int save_errno,
           const char *grouping, THOUSANDS_SEP_T thousands_sep,
           unsigned int mode_flags)
{

②.整体流程分析

  1. 申请specsbuf用于处理后续每一个占位符对应的特殊类型
  1. 申请argsbuf用于缓存不定参数的信息
  1. 一些局部变量的申请,如nspecs,nargs
  1. 根据locale信息初始化grouping和thousands_sep信息,便于后续打印数据
  1. 遍历format字符串中每一个占位符,初始化specs,specs_limit,nargs
  1. 校正nargs值
  1. 根据上面的信息初始化argsbuf.data的数据,填充数据与mode_flags相关
  1. 遍历所有的类型进行类型信息和size大小填充
  1. 知道类型后,填充数据,这时需要按顺序解析参数
  1. 遍历每一个参数,然后进行打印处理
  1. 最后all_done处理,释放buffer资源

中间的细节部分需要详细分析,我们专门开文进行讲解

static int printf_positional (...)
{
    // 申请specsbuf用于处理后续每一个占位符对应的特殊类型
    ...
    // 申请argsbuf用于缓存不定参数的信息
    ...
    // 一些局部变量的申请,如nspecs,nargs
    ...
    // 根据locale信息初始化grouping和thousands_sep信息,便于后续打印数据
    if (grouping == (const char *) -1)
    {
    ...
    }
    // 遍历format字符串中每一个占位符,初始化specs,specs_limit,nargs
      for (const UCHAR_T *f = lead_str_end; *f != L_('\0');
       f = specs[nspecs++].next_fmt)
    {
        ...
        specs = specsbuf.data;
        specs_limit = specsbuf.length / sizeof (specs[0]);
        ...
        nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg);
    }
    // 校正nargs值
    nargs = MAX (nargs, max_ref_arg);
    // 根据上面的信息初始化argsbuf.data的数据,填充数据与mode_flags相关
    {
        ...
        memset (args_type, (mode_flags & PRINTF_FORTIFY) != 0 ? '\xff' : '\0',
        nargs * sizeof (*args_type));
    }
    // 遍历所有的类型进行类型信息和size大小填充
      /* Fill in the types of all the arguments.  */
    for (cnt = 0; cnt < nspecs; ++cnt)
    {
        ...
        args_type[specs[cnt].data_arg] = specs[cnt].data_arg_type;
        args_size[specs[cnt].data_arg] = specs[cnt].size;
        ...
    }
    // 知道类型后,填充数据,这时需要按顺序解析参数
     /* Now we know all the types and the order.  Fill in the argument
     values.  */
    for (cnt = 0; cnt < nargs; ++cnt)
    switch (args_type[cnt])
      {
          ...
          args_value[cnt].pa_double = va_arg (*ap_savep, double);
          ...
      }
    // 遍历每一个参数,然后进行处理
      /* Now walk through all format specifiers and process them.  */
  for (; (size_t) nspecs_done < nspecs; ++nspecs_done)
    {
        ...
    }
    // 最后all_done处理,释放buffer资源
     all_done:
  scratch_buffer_free (&argsbuf);
  scratch_buffer_free (&specsbuf);
  return done;
}

(20)group_number---对数字进行分组

通过使用案例,我们其实可以很清晰地看出,将number.word转换为字符串string后,将work_buffer首地址作为front_ptr,string作为w,workend作为rear_ptr,调用了group_number,最后返回值赋值给到字符串string。

实际上做的操作是这样的,将字符串“1000000“转换为"1,000,000",假定规则为3个为一组,间隔符为','。

①.函数入参分析

  • CHAR_T *front_ptr:空闲buffer开始地址,到w截止
  • CHAR_T *w:数字字符串开始地址,到rear_ptr截止
  • CHAR_T *rear_ptr:整块work_buffer的末尾的下一个元素
  • const char *grouping:分组信息

grouping---指定形成每个组的位数,对于非货币量,这些数字将由千分位分隔符分隔。 举个例子,比如thousand_sep = ',',当我们表示1000000时: grouping = '\3',那就表示为'1,000,000'; grouping = '\1\2\3',那就表示为'1,000,00,0'; grouping = '\3\1',那就表示为'1,0,0,0,000'。

  • THOUSANDS_SEP_T thousands_sep:分组间隔符

thousands_sep---用于分隔非货币量小数点左侧的数字组的分隔符;

  • 返回值:返回分组之后的字符串开始地址,到rear_ptr截止
          /* Put the number in WORK.  */                                                                                                                 
          string = _itoa_word (number.word, workend, base,
                               spec == L_('X'));
          if (group && grouping)
            string = group_number (work_buffer, string, workend,
                                   grouping, thousands_sep);

/* Group the digits from W to REAR_PTR according to the grouping rules
   of the current locale.  The interpretation of GROUPING is as in
   `struct lconv' from <locale.h>.  The grouped number extends from
   the returned pointer until REAR_PTR.  FRONT_PTR to W is used as a
   scratch area.  */
static CHAR_T *
group_number (CHAR_T *front_ptr, CHAR_T *w, CHAR_T *rear_ptr,                                                                                            
          const char *grouping, THOUSANDS_SEP_T thousands_sep)
{

②.函数逻辑分析

  1. 初始化局部参数以及异常检测
  • tlen初始化为thousands_sep的长度,当然,这是针对宽字符的情况使用;
  • 如果grouping字符串的第一个字符异常,等于CHAR_MAX或着是负数,这时就不需要看后面的信息了,直接返回w指针,不进行分组处理;
  • len被赋值为第一个组的宽度,即grouping的第一个字符

细心的读者可能会注意到,这里不应该是第二个字符吗?因为前文说grouping = '\3',注意这里有一个背景,我们使用的是Glibc库的实现,我们来看看这里面的说明:

// glibc/locale/locale.h

/* Structure giving information about numeric and monetary notation. */

struct lconv

{

...

/* Each element is the number of digits in each group;

elements with higher indices are farther left.

An element with value CHAR_MAX means that no further grouping is done.

An element with value 0 means that the previous element is used

for all groups farther left. */

char *grouping;

...

};

这里就没有说明用引号间隔开来,而是grouping="321",这样的实现,所以我们的代码也是这样的

  /* Length of the current group.  */
  int len;
#ifndef COMPILE_WPRINTF
  /* Length of the separator (in wide mode, the separator is always a
     single wide character).  */
  int tlen = strlen (thousands_sep);
#endif

  /* We treat all negative values like CHAR_MAX.  */

  if (*grouping == CHAR_MAX || *grouping <= 0)
    /* No grouping should be done.  */
    return w;

  len = *grouping++;
  1. 拷贝处理避免数据被覆盖

将源数据[w,rear_ptr]拷贝到[front_ptr,s],因为我们最终要返回的数据一定会覆盖源数据,所以需要拷贝出来,然后将返回的字符指针指向rear_ptr,准备开始进行遍历

  /* Copy existing string so that nothing gets overwritten.  */
  memmove (front_ptr, w, (rear_ptr - w) * sizeof (CHAR_T));
  CHAR_T *s = front_ptr + (rear_ptr - w);
  
  w = rear_ptr;
  1. 遍历处理

开始循环时,w指向rear_ptr,s指向数字字符串的最后字符的下一个位置;

*--w = *--s,先各自减一,然后拷贝字符;

每完成一次拷贝,--len直到0,这时我们需要先输出分组间隔符thousands_sep,然后更新len信息

  /* Process all characters in the string.  */
  while (s > front_ptr)
    {
      *--w = *--s;
      if (--len == 0 && s > front_ptr)
    {                                                                                                                                                    
      /* A new group begins.  */
     ...
     }
     }
 return w;
}
  1. 插入分组间隔符

第一种情况(标准字符):

如果w!=s,说明现在的拷贝还没有覆盖到我们正在向前遍历的字符串,那么直接将分组间隔符赋值;

否则说明会导致字符串覆盖,work_buffer 不够大无法处理,跳转到copy_rest,把剩余的字符[front_ptr,s]拷贝到[w新,w旧],正好使用完所有的buffer。

第二种情况(宽字符):

逻辑基本一致,在插入分组间隔符时,由于是宽字符,所以不能直接赋值,需要拷贝cnt个字节。

      /* A new group begins.  */
#ifdef COMPILE_WPRINTF
      if (w != s)
        *--w = thousands_sep;
      else
        /* Not enough room for the separator.  */
        goto copy_rest;
#else
      int cnt = tlen;
      if (tlen < w - s)
        do
          *--w = thousands_sep[--cnt];
        while (cnt > 0);
      else
        /* Not enough room for the separator.  */
        goto copy_rest;
#endif

        copy_rest:
          /* No further grouping to be done.  Copy the rest of the
         number.  */
          w -= s - front_ptr;
          memmove (w, front_ptr, (s - front_ptr) * sizeof (CHAR_T));
          break;
  1. 更新group信息

如果grouping字符串的下一个信息是CHAR_MAX或小于0,那就copy_rest,拷贝剩余的字符,不再进行分组了;

如果grouping字符串的下一个信息不是'\0',即字符串类型为grouping=‘321’这样的,那就将信息赋值给len,然后grouping指针++;

其他情况,即grouping字符串的下一个信息是'\0'或其他情况,字符串类型为grouping=‘3’这样的,就将len赋值为grouping[-1],即前一个数据。

grouping[-1] 等价于*(grouping+(-1)),即*(grouping-1)

      if (*grouping == CHAR_MAX
#if CHAR_MIN < 0
           || *grouping < 0
#endif
           )
        {
        copy_rest:
          /* No further grouping to be done.  Copy the rest of the
         number.  */
          w -= s - front_ptr;
          memmove (w, front_ptr, (s - front_ptr) * sizeof (CHAR_T));
          break;
        }
      else if (*grouping != '\0')
        len = *grouping++;
      else
        /* The previous grouping repeats ad infinitum.  */
        len = grouping[-1];