cstdio 的源码学习分析 10- 格式化输入输出函数 fprintf--- 宏定义 / 辅助函数分析 04
cstdio 中的格式化输入输出函数
fprintf 函数的实现 vfprintf 中包含了相当多的宏定义和辅助函数,接下来我们一起来分析一下它们对应的源码实现。
函数逻辑分析---vfprintf
2.宏定义/辅助函数分析
(14)__printf_fp_spec---打印一个特定类型的变量
入参
FILE *fp:文件流对象
const struct printf_info *info:本次识别的 format 占位符对应解析出的相关信息
const void *const *args:本次占位符对应的参数列表
这个函数是其中比较核心的函数,在每一次处理 format string 时,提取出一个占位符,并填充相关识别信息到 printf_info 结构体中,传入其对应的参数指针,对当前的特定类型变量进行打印。
由于这个函数承载了大量的细节处理,所以还是通过函数调用的方式进行实现的,根据对应的类型,如果是'a'或'A'即 16 进制打印,那需要调用__printf_fphex 完成相关的打印工作,否则统一使用__printf_fp 完成相关的打印工作,这里我们将__printf_fphex/__printf_fp 的核心实现留到后文解析,目前就当成这个函数能够完成一个特定类型数据的打印即可。
当然,在这里,我们有必要看一下 printf_info 结构体的内容,如下:
参照前文所说一个 format 占位符的格式
%[flags][width][.precision][length]specifier
可以看到,在 printg_info 结构体中,实际上是按顺序包含我们在打印过程中所需要的所有控制信息,vfprintf 函数的核心逻辑就是识别字符串并构造这样一个结构体,然后传给__printf_fp_spec 进行打印处理,不断循环,直到处理结束。
(15).is_longlong 与 is_long_num---针对 32 位和 64 位的兼容机制
如果 LONG_MAX == LONG_LONG_MAX,说明是 32 位机器,需要将 is_longlong 置为 0,否则与 is_long_double 保持一致;
如果 INT_MAX == LONG_MAX,说明是 32 位机器,需要将 is_long_num 置为 0,否则与 is_long 保持一致。
(16)跳转表机制---用于处理 format 占位符的识别
回到上面提到的 vfprintf 的核心任务,一个是识别 format 占位符构造 printf_info 结构体,一个是打印该种特定类型的变量,这个跳转表机制就是为了识别 format 占位符而创建的。
%[flags][width][.precision][length]specifier
对于这样的一个字符串类型识别,我们通常会采用自动机的方式进行识别,设定多个状态,每个状态代表一个识别位,状态之间的跳转需要条件,直到最后识别到specifier
类型,这时,我们就能够完全解析出所有字段,上面构想的方法其实就是有限状态自动机的方法。
但是,因为 C 标准中规定了上面五个字段中可以有很多种组合,这个状态数量的叠加将是非常巨大的,我们能否寻找到一种比较简单的方式实现这个逻辑呢?
答案是有的,在 C 语言中有一种语法深为人所诟病,那就 goto 语句,可以进行无条件跳转到设定好的标号处,当然大部分情况下我们是不建议使用的,但是它在这种场景下就很契合,试想一下,我们的有限状态自动机中每一个状态是否就是一个标号呢?在这个标号中处理当前符号的识别,处理完成之后,移动到下一个字符,并通过我们设定好的跳转表跳转到下一个标号处进行处理,这个过程中,我们需要维护好的就是这个关键的跳转表,它记录了当前符号的下一个处理标号的位置,跳转表充当了有限状态自动机中触发状态变化函数的作用。
我们来看一看具体代码中的例子:
这里我们以打印一个 %c 为例
首先,在 vfprintf 中,我们首先找到第一个 %号;
然后输出第一个 %号之前的所有字符串,
这些字符串是不需要解析的
定义跳转表
处理 %后面的第一个字符
JUMP 宏解析
接下来我们来看看这个 JUMP 宏是如何进行解析的,注意:按照我们例子,现在传入的是字符 c
这里核心是找到 ptr(即当前字符 c 对应的 label 地址),然后 goto *ptr 跳转。
首先是判断输入字符是否在跳转范围内,跳转表是从 L_(' ')到 L_('z')的所有字符(看到这里不由得对 ASCII 码设计者和 C 语言中 print format 关键字设置的逻辑感到赞叹,都是相互联系的,否则这里的代码就更加混乱不能理解了)。
如果不在这其中,那我们默认返回 REF (form_unknown),即 do_form_unknown 与 do_form_unknown 的地址差,为 0;
如果在其中,那么我们查表可知字符’c‘对应的值为 20,所以我们需要访问表 table[20],注意现在的表是传入的 step0_jumps
查表 step0_jumps[20],可以看到 index20 的位置是 REF (form_character),根据 REF 的解析格式,应该是
&&do_form_character - &&do_form_unknown,接下来我们需要找到 do_form_character 的标号位置进行处理
LABEL(form_character)的处理逻辑
注意到有专门的宏 LABEL 定义标号,所以我们要找 LABEL(form_character)
这个标号下的处理逻辑就比较明晰了:
先处理宽字符情况,跳转到 LABEL (form_wcharacter);
因为标准字符只占用一个字节,所以对齐宽度先减一;
如果指定非左对齐,那么则需要在左侧增加空格进行对齐;
然后调用 outchar 输出当前字符,当前字符的获取通过 process_arg_int 得到(实际上就是通过 va_arg 获取 int 型数据)
最后进行左对齐的处理,在右侧增加空格进行对齐。
因为跳转表的机制过于繁琐,上面我们只是以 %c 这样一个小例子作为解析方式,后续会专门写文章说明跳转表的机制,因为跳转表不止一张,其中的跳转逻辑也不尽相同,读者主要要理解这种有限状态自动机转换的思想。
版权声明: 本文为 InfoQ 作者【桑榆】的原创文章。
原文链接:【http://xie.infoq.cn/article/20aa94669ffffe90fb250229d】。文章转载请联系作者。
评论