持续创作,加速成长!这是我参与「掘金日新计划 · 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;
- const char *grouping:描述数字组大小的字符串,即多少个数字为一组进行打印,通常我们会看到国外会将数字三个一组进行打印,这里可以参考C++学习------clocale头文件的源码学习01---类型定义
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:千位分隔符,即三位表示时,中间的间隔符,可以参考C++学习------clocale头文件的源码学习01---类型定义
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)
{
②.整体流程分析
- 申请specsbuf用于处理后续每一个占位符对应的特殊类型
- 申请argsbuf用于缓存不定参数的信息
- 一些局部变量的申请,如nspecs,nargs
- 根据locale信息初始化grouping和thousands_sep信息,便于后续打印数据
- 遍历format字符串中每一个占位符,初始化specs,specs_limit,nargs
- 校正nargs值
- 根据上面的信息初始化argsbuf.data的数据,填充数据与mode_flags相关
- 遍历所有的类型进行类型信息和size大小填充
- 知道类型后,填充数据,这时需要按顺序解析参数
- 遍历每一个参数,然后进行打印处理
- 最后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)
{
②.函数逻辑分析
- 初始化局部参数以及异常检测
- 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++;
- 拷贝处理避免数据被覆盖
将源数据[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;
- 遍历处理
开始循环时,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;
}
- 插入分组间隔符
第一种情况(标准字符):
如果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;
- 更新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];