写点什么

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

作者:桑榆
  • 2022-10-12
    广东
  • 本文字数:3570 字

    阅读完需:约 12 分钟

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 占位符中最重要的部分,因为它标识了当前占位符的类型以及如何解释对应该位置的参数。


flags---格式化 flags 信息


width---格式化填充宽度


precision---精度信息


length---类型的长度扩展信息

如我们用 u 表示无符号整数,那么可以叠加 lu,表示 unsigned long int,llu 表示 unsigned long long int.

具体的组合如下,注意其中我们不常使用的如 jd 表示 intmax_t,td 表示 ptrdiff_t。


函数入口分析

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

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

实际上经过 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. */intvfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags){...}
复制代码

函数逻辑分析---vfprintf

1.整体分析

上一节中我们并未标识行号,大家可能对这个函数的大小没有足够的概念,这里我们将主要流程与相关行号的代码也一并贴出,可以看到是一个超过 500 行的函数,基本流程如下:

  1. 局部参数定义---包括所有用到的变量和扩展结构体;

  2. 各类参数预处理---包括参数检查,第一个 format 参数的识别等;

  3. 开始遍历处理 format 字符串直到识别到'\0';

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

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

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

  4. 一些 label 定义---如 do_positional,all_done;

  5. 返回结果

注意:这个超过 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的识别工作//处理某一种format1007 /* Process current format. */1008 while (1)1009 {// 对该种format的处理逻辑1080 /* If we are in the fast loop force entering the complicated1081 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 定义->返回结果)。

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

桑榆

关注

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

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

评论

发布
暂无评论
cstdio的源码学习分析10-格式化输入输出函数fprintf整体分析_源码刨析_桑榆_InfoQ写作社区