printf,iosteam,以及未来的std::format

格式化输出,一直是很平常但非常重要的功能。任何一个项目,必然会遇到这个技术点。

printf是开创先河之物

printf是古老的技术,但是速度不慢。它的核心思路是看到%符号,就是要被替换的内容。%后面是格式说明符,由一些预定义的字符组成。

例如:%+#08*.4hd   如何理解它呢?
格式说明符的通用语法为:%[flags][width][.precision][length]specifier  由4个可选群组[flags][width][.precision][length],以及一个必选项specifier组成。
[flags]群组由以下字符任意多个组成:  + - # 0 space
[width]群组由以下字符的任意多个组成:0,1,2,3,4,5,6,7,8,9,*     例如:%8*d
[.precision]群组由以下字符的任意多个组成:0,1,2,3,4,5,6,7,8,9,*  但是必须是一个点作为前导字符。例如:%.4d
[length]群组由以下集合的中元素选一个构成{h,hh,l,ll, z,t,L }  例如:%hd
必选项specifier,由以下集合的中元素选一个构成{d,i,u,o,x,X,f,F,e,E,g,G,a,A,c,s,p,n,%}  例如:%d

如何用人脑翻译这些字符的含义,或者组织一个字符群组来表达自己的意思呢?
首先,不同群组选用的字符有意的避开了冲突,例如d这个字符,只能存在于必选项specifier,而不会出现在[flags]群组中。同理#这个字符也只能出现在[flags]群组中。
第二,群组由有顺序的,[flags]群组永远排在前面,必选项specifier永远排在最后。例如:%3#dh  就是一个非法的格式,因为h是属于[length]群组的,它却排在d之后。 
第三,数字0可以位于[flags][width][.precision]这三个群组,如何判断字符0到底是什么意思呢?这需要用到一点状态机的思想。

状态机处理逻辑:遇到%后,立刻进入格式说明符状态,初始状态是[flags],此时遇到一个0显然就是一个flag。如果此时遇到数字(1-9),就进入[width]子状态,如果遇到d就进入specifier子状态
大概有了这个思路后,有两个好处:一个是读写%+#08*.4hd之类的东西很清晰,它就是%[+#0] [8*] [.4] [h] d , 然后是手写家酿的printf也很有思路了。

下面对这些格式符号做一些简要说明

[flags] - + space # 0 这5个flag
其中:  - 0 是一组的,- 指定左对齐 ; 而0控制左侧填充物为0。如果左对齐,则没有填充物0。填充物0,意味着右对齐。因此 - 0 是互斥关系。但是仍不能阻挡同时填写- 0 ,此时-的优先级高。
printf("%-010d", 1234); //等价于 printf("%-10d",1234 ); 输出:1234______    (注:为了突出空格,用_代替一个空格位)

+ space是一组,+ 指定正负号全显示(默认是只显示正号),space是指定(正数前填空格),默认是只有负数前填-。显然,+ 和 space是互斥的(二选一)。两者同时存在时,+ 的优先级高
printf("%+ 10d", 1234); //等价于 printf("%+10d", 1234 ); 输出:_____+1234    (注:为了突出空格,用_代替一个空格位)
printf("%+    10d", 1234);  //仍旧输出:_____+1234  这说明了 printf函数的解析方式是逐个字符分析的,容许重复flag

# 用于指定打开0x前缀显示等用途,自成一派,与其它flag不冲突。
printf("%#10x", 0x12AB);  //输出:____0x12ab  全小写16进制数    printf("%10x", 0x12AB);  //输出:______12ab  全小写16进制数

[width] * 或者 一个数字(正数)。
printf("%*d", 10, 1234);  //输出:______1234   长度由1234参数的前一个参数10指定

* 与 数字6同时存在,以数字优先级高。但是此时参数表的10不可省略,否则打印不出1234。例如:printf("%*6d", 10, 1234);   //输入:______1234   长度由1234参数的前一个参数10指定

[.precision] 指定精度
printf("%6.4d", 123);  //对于整数值,.4代表至少转换为4位,不足的前补0. 输出:__0123
printf("%*.*d", 6, 4, 123);  //第一个*需要实参6,第二个*需要实参4。这个例子说明 .*的用法

[length]  长度。用于指定实参的数据类型
printf("%d", 123); //空的长度(specifier之前,d之前没有长度信息),表示按照d的默认长度(int)。
print(%hd,123);  //长度h代表short,具体的数据类型由hd共同决定(printf的API文档里,有个表,查表得到[length]specifier组合对应的数据类型)
printf("%hhd", 1000);  输出-24.  1000的二进制为3E8, 截断成char为E8, E8对应十进制负24。格式符和实参不匹配,有时会使va_list指针前进步长出现错误。

iostream是不成熟的产物

iostream没有占位符概念,用<<运算符重载。有个诙谐的评语:chevron hell (雪佛龙地狱)。chevron的意思是箭头(特别是肩章的箭头)
std::cout << std::setprecision(2) << std::fixed << "x=" << 1.23 << " y=" << 2.34 << " z=" << 3.45 << "\n";   //样式模板 x=%f y=%f z=%f,格式:精度2位 数据:1.23  2.34  3.45
printf是利用了可变个数参数语法,虽然符合人类的习惯,但是类型不那么安全。iostream类型安全了,但是样式模板,数据,格式全部混合成一锅粥了。

现代化的std:format

既要学习printf那样符合人类习惯的写法,又类型安全,在C++98之前是不可能的。现代化的C++涌现出可变参数个数的模板,右值引用等新技术,让现代化的printf成为可能。
人们发现python的格式化语法比%好,因此把python的{}这个符号引入了。例如:  printf("The answer is {}.", 42);   print("I'd rather be {1} than {0}.", "right", "happy");  //支持参数序号
print("Hello, {name}! The answer is {number}. Goodbye, {name}.\n",  "name"_a="Tom",  "number"_a=42);  //支持参数名称
背靠现代化C++的基础设施,format库可以做的功能强大,效率不输(甚至部分效率更高)C的printf。

哪里获得?

本文编写时,std::format还未实现。可见做出一个各方都满意的库(特别是格式化打印这样关系到程序员日常生活的库),是非常困难的。有了iostream的前车之鉴(被人喷了几十年)
这个库的开发者可要悠着点了。

https://github.com/fmtlib/fmt  这个库非常接近std::format,迫不及待尝鲜的可以拿它来做实验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值