cstdio的源码学习分析11-格式化输入输出函数fprintf整体分析

211 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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占位符中最重要的部分,因为它标识了当前占位符的类型以及如何解释对应该位置的参数。

specifierOutputExample
d or iSigned decimal integer/有符号十进制整数392
uUnsigned decimal integer/无符号十进制整数7235
oUnsigned octal/无符号8进制数610
xUnsigned hexadecimal integer/无符号16进制整数(字母用小写表示)7fa
XUnsigned hexadecimal integer (uppercase)/无符号16进制整数(字母用大写表示)7FA
fDecimal floating point, lowercase/十进制浮点数(小写)392.65
FDecimal floating point, uppercase/十进制浮点数(大写)392.65
eScientific notation (mantissa/exponent), lowercase/科学计数法3.93E+02
EScientific notation (mantissa/exponent), uppercase/科学计数法3.93E+02
gUse the shortest representation: %e or %f/使用%e或%f中最短的392.65
GUse the shortest representation: %E or %F/使用%E或%F中最短的392.65
aHexadecimal floating point, lowercase/十六进制浮点数(字母用小写表示)-0xc.90fep-2
AHexadecimal floating point, uppercase/十六进制浮点数(字母用大写表示)-0XC.90FEP-2
cCharacter/字符a
sString of characters/字符串sample
pPointer address/指针地址b8000000
n不进行打印;对应的参数必须是一个指向有符号int数的指针,将到目前位置写入的字符数存储在该指针指向的变量中。
%A % followed by another % character will write a single % to the stream. 打印%符号%

flags---格式化flags信息

flagsdescription
-给定字段宽度内向左对齐;默认的对齐方式是向右对齐。
+强制使用加号或减号(+或-)预处理结果,即使是正数。默认情况下,只有负数前面带有-符号。
(space)如果不写符号,则在值之前插入空格
#o、x或X说明符一起使用时,对于不同于零的值,值前面分别加0、0x或0X。 与a、A、e、E、f、F、g或G一起使用时,即使后面没有数字,也会强制写入输出包含小数点。默认情况下,如果小数点后面没有数字,则不会写入小数点。
0当指定填充时,左侧用零(0)代替空格填充数字。

width---格式化填充宽度

widthdescription
(number)要打印的最小字符数。如果要打印的值短于此数字,则会用空格填充结果。即使结果较大,也不会截断该值。
*宽度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的,即需要在后面的附加参数中填充此处希望打印的宽度。

precision---精度信息

.precisiondescription
.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
lengthd iu o x Xf F e E g G a Acspn
(none)intunsigned intdoubleintchar*void*int*
hhsigned charunsigned charsigned char*
hshort intunsigned short intshort int*
llong intunsigned long intwint_twchar_t*long int*
lllong long intunsigned long long intlong long int*
jintmax_tuintmax_tintmax_t*
zsize_tsize_tsize_t*
tptrdiff_tptrdiff_tptrdiff_t*
Llong double

函数入口分析

这里通过Glibc的函数定义和调用关系,我们可以看到,__fprintf是fprintf的别名,实际上最后是调用的__vfprintf_internal函数,其实这也是符合我们的逻辑的,可变参数函数一定包含固定参数的函数,所以其实我们只需要实现一次可变参数函数就好。

注意:这里我们需要关注__vfprintf_internal的入参需求:

  • FILE*是文件流对象指针
  • format是我们设定的format参数

实际上经过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行的函数,基本流程如下:

  1. 局部参数定义---包括所有用到的变量和扩展结构体;
  1. 各类参数预处理---包括参数检查,第一个format参数的识别等;
  1. 开始遍历处理format字符串直到识别到'\0';

    a. 进行format的识别工作,要识别出上面所说的%[flags][width][.precision][length]specifier

    b. 处理当前识别出的format,需要找到对应的参数,然后以对应的格式化信息进行处理

    c. 将格式化后的数据写入约定的buffer中

  1. 一些label定义---如do_positional,all_done;
  1. 返回结果

注意:这个超过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定义->返回结果)。