17.3 格式化输出
怎么让屏幕(日志)上打印出来的浮点数只保留两位小数?怎么让输出的金钱表格对其且前面补零?
在 C 语言中,我们有无敌的 printf("%05d %.2f");但随着 C++ 中引入了 std::cout 之后,如何进行这些格式控制就成为了一个被广泛吐槽的繁琐话题。
本节我们将看看传统的 C++ 格式化方法,以及在 C++20 中迎来终极解决的大杀器特性。
传统 C++:流控制符 (Manipulators)
Section titled “传统 C++:流控制符 (Manipulators)”在使用 std::cout(或是任何其它派系的 fstream/stringstream)时,可以用特殊的函数对象插入到流里面,直接修改在它这道“关卡”后面经过的数据的表现形式。它们全在定义在 <iomanip> 头文件里。
最知名的例子就是大名鼎鼎的 std::endl,它其实就是个控制符(它不仅插入了换行符 \n,还在底层执行了至关重要的刷新冲出缓冲区 flush 动作)。
#include <iostream>#include <iomanip> // 包含了所有带参数的流控制大炮
int main() { double pi = 3.14159265;
// 1. 设置浮点数精度(最常用) // fixed: 取消科学计数法; setprecision(2): 保留 2 位小数 std::cout << "圆周率大约是: " << std::fixed << std::setprecision(2) << pi << "\n"; // 注意:一旦设置了 fixed 这类的控制标志,它在这个 cout 身上会永久生效遗留! // 除非你自己再发指令撤销
// 2. 补零、对齐与宽度规范 int score = 42; // setw(5): 要求下一个印出来的东西死死占满 5 个格子的宽度(这个属性印一次就立马自动失效) // setfill('0'): 如果字短没靠满,用 '0' 填死前面那些空着的格子 std::cout << "玩家总分: " << std::setfill('0') << std::setw(5) << score << "\n"; // 结果将打出 00042!
// 3. 多进制切换输出打印 int val = 255; std::cout << "十进制: " << std::dec << val << "\n"; std::cout << "十六进制: " << std::hex << val << "\n"; // 输出 ff std::cout << "八进制: " << std::oct << val << "\n"; // 输出 377
return 0;}传统的流控制看着啰嗦冗长,而且如果不注意,会把整个系统原先其它部件正常的默认格式污染拖下水,写起来极易串色。
究极杀手锏:C++20 std::format
Section titled “究极杀手锏:C++20 std::format”C++ 委员会也受不了 iomanip 的繁冗了。在近十年的发展中,社区普遍广泛引进了开源库 fmt 的使用。到了 C++20,将其正式大举收编入标准库立正:std::format!
它完美地融合了 C 语言 printf 那种格式化缩写的极其短小快捷,同时又具备了现代泛型安全(你不必记住什么指代 int、指代 const char *,哪怕给错了也不会像 printf 发疯产生未定义段错误崩溃越界)、并且通吃标准库任意新老类(只要重载配制过)。
#include <iostream>#include <string>#include <format> // 只有在 C++20 及以上的编译器标准中可用!
int main() { std::string name = "Alice"; int age = 24; double money = 12.3456;
// Python 极其眼熟的语法重现!在字串符里留下一对对挖空大嘴巴 {} // std::format 会负责返回一整个缝合好的完美 std::string。没有任何污染! std::string msg = std::format("{} 已经 {} 岁了。", name, age); std::cout << msg << "\n"; // => Alice 已经 24 岁了。
// 指点江山参数坑位指定! std::cout << std::format("第二个人是 {1},第一个是 {0}。\n", "A", "B"); // => 第二个人是 B,第一个是 A。
// ---------------------------------------------------- // // 超强格式化语法内嵌:在花括号里放入冒号 : 开启神技定制
// 1. 浮点精度定制 ({:.2f} 为保留 2 位死局小数) std::cout << std::format("余额:{:.2f} 元 \n", money); // 12.35 元 (自动进位四舍五入)
// 2. 宽度、对齐填充 (超强!做漂亮打印终端仪表盘的救星) // 规定占域 10 个格。左对齐 < (或者是右对齐 > ,亦或者居中 ^ );不够的部分补 * std::cout << std::format("[{:*<10}]\n", "Test"); // => [Test******] std::cout << std::format("[{:=>10}]\n", "Test"); // => [======Test] std::cout << std::format("[{:_^10}]\n", "Test"); // => [___Test___]
// 3. 数字转换 int n = 255; // 把 n 转成带前缀的漂亮大写 16 进制 (#X 搞定),补 0 保 6 格宽 std::cout << std::format("{:#06X}\n", n); // => 0X00FF
return 0;}毫不夸张地说,std::format 是 C++20 日常体验提升幅度最大的巨无霸功能。
此外在 C++23 中更进一步下放了 std::print(...) 以及 std::println(...) 函数,直接能一口气吞带替换掉 std::cout << std::format(...) 这副长面孔。这是 C++ 一扫“控制台打印字符语法陈旧笨重”非议的最重大胜利姿态。