持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
cstdio中的格式化输入输出函数
stdio.h中定义了一系列格式化输出函数,接下来我们一起来分析一下fprintf对应的源码实现。
-
固定参数类型
- fprintf:将格式化数据写入文件流对象FILE中
- fscanf::文件流对象FILE中读取格式化数据
- printf:将格式化数据写入到stdout(标准输出)FILE 对象中
- scanf:从stdin(标准输入)FILE对象中读取数据
- snprintf:将格式化数据输出到buffer中
- sprintf:将格式化数据输出到字符串中
- sscanf:从字符串中读取格式化数据
-
可变参数类型(函数与固定参数一一对应,只是接受可变参数列表作为输入或输出)
- vfprintf:将格式化数据写入文件流对象FILE中
- vfscanf::文件流对象FILE中读取格式化数据
- vprintf:将格式化数据写入到stdout(标准输出)FILE 对象中
- vscanf:从stdin(标准输入)FILE对象中读取数据
- vsnprintf:将格式化数据输出到buffer中
- vsprintf:将格式化数据输出到字符串中
- vsscanf:从字符串中读取格式化数据
函数作用简介---fprintf
将对应的format格式参数与附加的参数组成字符串,写入到指定的FILE对象中。
- 若写入成功,则返回写入的总字符数;
- 若写入失败,则将在FILE对象中值错误信息,该错误信息可由ferror函数检测到,返回一个负数;
- 若是多字节符号编码错误(如宽字符),则会将errno置为EILSEQ,返回一个负数。
int fprintf ( FILE * stream, const char * format, ... );
注意:上面参数中的format有许多规定好的格式
format 占位符的相关知识
format占位符的一般形式:%[flags][width][.precision][length]specifier
specifier---特有类型
这是format占位符中最重要的部分,因为它标识了当前占位符的类型以及如何解释对应该位置的参数。
| specifier | Output | Example |
|---|---|---|
| d or i | Signed decimal integer/有符号十进制整数 | 392 |
| u | Unsigned decimal integer/无符号十进制整数 | 7235 |
| o | Unsigned octal/无符号8进制数 | 610 |
| x | Unsigned hexadecimal integer/无符号16进制整数(字母用小写表示) | 7fa |
| X | Unsigned hexadecimal integer (uppercase)/无符号16进制整数(字母用大写表示) | 7FA |
| f | Decimal floating point, lowercase/十进制浮点数(小写) | 392.65 |
| F | Decimal floating point, uppercase/十进制浮点数(大写) | 392.65 |
| e | Scientific notation (mantissa/exponent), lowercase/科学计数法 | 3.93E+02 |
| E | Scientific notation (mantissa/exponent), uppercase/科学计数法 | 3.93E+02 |
| g | Use the shortest representation: %e or %f/使用%e或%f中最短的 | 392.65 |
| G | Use the shortest representation: %E or %F/使用%E或%F中最短的 | 392.65 |
| a | Hexadecimal floating point, lowercase/十六进制浮点数(字母用小写表示) | -0xc.90fep-2 |
| A | Hexadecimal floating point, uppercase/十六进制浮点数(字母用大写表示) | -0XC.90FEP-2 |
| c | Character/字符 | a |
| s | String of characters/字符串 | sample |
| p | Pointer address/指针地址 | b8000000 |
| n | 不进行打印;对应的参数必须是一个指向有符号int数的指针,将到目前位置写入的字符数存储在该指针指向的变量中。 | |
| % | A % followed by another % character will write a single % to the stream. 打印%符号 | % |
flags---格式化flags信息
| flags | description |
|---|---|
| - | 给定字段宽度内向左对齐;默认的对齐方式是向右对齐。 |
| + | 强制使用加号或减号(+或-)预处理结果,即使是正数。默认情况下,只有负数前面带有-符号。 |
| (space) | 如果不写符号,则在值之前插入空格 |
| # | o、x或X说明符一起使用时,对于不同于零的值,值前面分别加0、0x或0X。 与a、A、e、E、f、F、g或G一起使用时,即使后面没有数字,也会强制写入输出包含小数点。默认情况下,如果小数点后面没有数字,则不会写入小数点。 |
| 0 | 当指定填充时,左侧用零(0)代替空格填充数字。 |
width---格式化填充宽度
| width | description |
|---|---|
| (number) | 要打印的最小字符数。如果要打印的值短于此数字,则会用空格填充结果。即使结果较大,也不会截断该值。 |
| * | 宽度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的,即需要在后面的附加参数中填充此处希望打印的宽度。 |
precision---精度信息
| .precision | description |
|---|---|
| .number | 对于整数说明符(d,i,o,u,x,X):precision指定要写入的最小位数。如果要写入的值短于此数字,则会用前导零填充结果。即使结果更长,也不会截断该值。精度为0表示没有为值0写入字符。 对于a、A、e、E、f和F说明符:这是小数点后要打印的位数(默认为6)。 对于g和G说明符:这是要打印的最大有效数字数。 对于s:这是要打印的最大字符数。默认情况下,将打印所有字符,直到遇到结束的空字符。 如果指定的周期没有明确的精度值,则假定为0。 |
| .* | 精度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的。与上面width中的*用法一致,需要后面的附加参数指定精度。 |
length---类型的长度扩展信息
如我们用u表示无符号整数,那么可以叠加lu,表示unsigned long int,llu表示unsigned long long int.
具体的组合如下,注意其中我们不常使用的如jd表示intmax_t,td表示ptrdiff_t。
| specifiers | |||||||
| length | d i | u o x X | f F e E g G a A | c | s | p | n |
| (none) | int | unsigned int | double | int | char* | void* | int* |
| hh | signed char | unsigned char | signed char* | ||||
| h | short int | unsigned short int | short int* | ||||
| l | long int | unsigned long int | wint_t | wchar_t* | long int* | ||
| ll | long long int | unsigned long long int | long long int* | ||||
| j | intmax_t | uintmax_t | intmax_t* | ||||
| z | size_t | size_t | size_t* | ||||
| t | ptrdiff_t | ptrdiff_t | ptrdiff_t* | ||||
| L | long double |
函数入口分析
这里通过Glibc的函数定义和调用关系,我们可以看到,__fprintf是fprintf的别名,实际上最后是调用的__vfprintf_internal函数,其实这也是符合我们的逻辑的,可变参数函数一定包含固定参数的函数,所以其实我们只需要实现一次可变参数函数就好。
注意:这里我们需要关注__vfprintf_internal的入参需求:
- FILE*是文件流对象指针
- format是我们设定的format参数
- arg是我们输入的附加参数的 va_list(这里可以参考C++学习---变长参数(stdarg.h)的实现原理 )
实际上经过va_start宏处理之后,arg指向的就是format参数后面的位置,即第一个可变参数
// 一种默认实现
#define va_list char*
#define va_start(ap,arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap,t) (*(t*)((ap+=sizeof(t))-sizeof(t)))
#define va_end(ap) (ap=(va_list)0)
- 0是增加的mode信息
// glibc/libio/stdio.h
/* Write formatted output to STREAM.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int fprintf (FILE *__restrict __stream,
const char *__restrict __format, ...);
// glibc/stdio-common/fprintf.c
/* Write formatted output to STREAM from the format string FORMAT. */
/* VARARGS2 */
int
__fprintf (FILE *stream, const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = __vfprintf_internal (stream, format, arg, 0);
va_end (arg);
return done;
}
ldbl_hidden_def (__fprintf, fprintf)
ldbl_strong_alias (__fprintf, fprintf)
如何跳转到__vfprintf_internal的实现?
Glibc中为了做兼容操作,用了相当多的宏,我们接着追__vfprintf_internal的定义,会发现原本的函数又跳到了vfprintf,这里针对宽字符做了兼容处理,我们暂时只需要关注标准字符的处理即可。
至此,我们终于看到fprintf的函数最终函数实现,跳转路径如下:
fprintf->__fprintf->__vfprintf_internal->vfprintf
后续我们就实际分析vfprintf函数的实现流程
// glibc/stdio-common/vfprintf-internal.c
#ifndef COMPILE_WPRINTF
# define vfprintf __vfprintf_internal
...
#else
# define vfprintf __vfwprintf_internal
#endif
/* The function itself. */
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
...
}
函数逻辑分析---vfprintf
1.整体分析
上一节中我们并未标识行号,大家可能对这个函数的大小没有足够的概念,这里我们将主要流程与相关行号的代码也一并贴出,可以看到是一个超过500行的函数,基本流程如下:
- 局部参数定义---包括所有用到的变量和扩展结构体;
- 各类参数预处理---包括参数检查,第一个format参数的识别等;
-
开始遍历处理format字符串直到识别到'\0';
a. 进行format的识别工作,要识别出上面所说的
%[flags][width][.precision][length]specifierb. 处理当前识别出的format,需要找到对应的参数,然后以对应的格式化信息进行处理
c. 将格式化后的数据写入约定的buffer中
- 一些label定义---如do_positional,all_done;
- 返回结果
注意:这个超过500行的函数中还有许多子函数和宏的调用,所以是一个相对比较复杂的函数,这里我们需要一个一个部分地来进行分析。
// 源代码位于glibc/stdio-common/vfprintf-internal.c中
681 /* The function itself. */
682 int
683 vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
684 {
// 局部参数定义
...
720 /* Orient the stream. */
721 #ifdef ORIENT
722 ORIENT;
723 #endif
// 各类参数预处理
...
774 /* Use the slow path in case any printf handler is registered. */
775 if (__glibc_unlikely (__printf_function_table != NULL
776 || __printf_modifier_table != NULL
777 || __printf_va_arg_table != NULL))
778 goto do_positional;
// 开始遍历处理format字符串
780 /* Process whole format string. */
781 do
782 {
// 中间进行format的识别工作
//处理某一种format
1007 /* Process current format. */
1008 while (1)
1009 {
// 对该种format的处理逻辑
1080 /* If we are in the fast loop force entering the complicated
1081 one. */
1082 goto do_positional;
1083 }
...
// 将真实的数据写出到字符串中
1095 /* Write the following constant string. */
1096 outstring (end_of_spec, f - end_of_spec);
1097 }
1098 while (*f != L_('\0'));
// 一些lable定义
1100 /* Unlock stream and return. */
1101 goto all_done;
1102
1103 /* Hand off processing for positional parameters. */
1104 do_positional:
1105 done = printf_positional (s, format, readonly_format, ap, &ap_save,
1106 done, nspecs_done, lead_str_end, work_buffer,
1107 save_errno, grouping, thousands_sep, mode_flags);
1108
1109 all_done:
1110 /* Unlock the stream. */
1111 _IO_funlockfile (s);
1112 _IO_cleanup_region_end (0);
1113
// 最后返回结果
1114 return done;
1115 }
总结
鉴于vfprintf这个函数的复杂性,笔者对文章进行了拆解,本文主要是对fprintf实现的整体分析,具体该函数中每一个步骤是如何完成的,如
- 有哪些宏的使用;
- 是如何识别出对应的format占位符的;
- 是如何构造我们最终的字符串的;
- ......
这些问题我们都留到后文一一细细分析,读者在本文的阅读中可以重点看看format参数的含义,以及vfprintf函数的5个大致流程(局部参数定义->各类参数预处理->遍历format字符串进行处理->label定义->返回结果)。