在日常开发中,把数据打印到屏幕上时,我们经常需要控制输出的显示格式,比如把商品单价 19.8 保留两位小数显示成 19.80,或者让多列数据上下整齐对齐。
前面学过的 \t 转义字符虽然能拉开间隔,但遇到长短不一的文字时很容易错位。如果想要准确控制小数位数、固定列宽或指定对齐方向,我们需要引入 <iomanip> 头文件。其中,iomanip 是英文 “input/output manipulators” 的缩写,代表输入输出格式控制。
C++ 中最常用的输出格式控制工具如下表所示。
| 工具名称 | 说明 | 常见用法 |
|---|---|---|
| std::fixed | 采用定点小数形式输出浮点数 | std::cout << std::fixed; |
| std::setprecision(n) | 配合 std::fixed 设置保留 n 位小数 | std::cout << std::setprecision(2); |
| std::setw(n) | 设置下一项输出的最小字符宽度 | std::cout << std::setw(8); |
| std::left | 在指定宽度内靠左对齐 | std::cout << std::left; |
| std::right | 在指定宽度内靠右对齐(默认) | std::cout << std::right; |
| std::setfill(ch) | 设置宽度不足时的填充字符 | std::cout << std::setfill('0'); |
需要注意的是,调用 std::setprecision()、std::setw()、std::setfill() 等带参数的工具时,除了包含 <iostream> 头文件外,还必须在代码开头写上 #include <iomanip>,然后把它们放进 std::cout 的输出流中使用。
控制小数位数
在输出浮点数(小数)时,最常见的需求就是指定小数点后面保留几位小数。在 C++ 中,我们通常把 std::fixed 和 std::setprecision(n) 搭配起来使用。
1. 固定保留 n 位小数(推荐)
std::fixed 表示采用固定的小数点形式输出浮点数,配合后面的 std::setprecision(n) 即可指定小数点后保留的位数。
语法:
std::cout << std::fixed << std::setprecision(n) << 浮点数;说明:
其中 precision 是英文 “精度” 的意思,参数 n 是一个整数,表示保留 n 位小数。如果小数原本的位数不足 n 位,末尾会自动补 0。如果原本的位数超过 n 位,则会自动进行四舍五入。
示例 1:保留指定位数的小数
#include <iomanip>
#include <iostream>
int main() {
double temperature = 19.8;
double pi = 3.1415926;
std::cout << std::fixed;
std::cout << std::setprecision(2) << temperature << '\n';
std::cout << std::setprecision(3) << pi << '\n';
return 0;
}运行结果如下。
19.80
3.142分析:
在这个例子中,我们先通过 std::fixed 开启定点小数格式,接着使用 std::setprecision(2) 保留 2 位小数,因此 19.8 末尾自动补 0 显示为 19.80。下一行改为保留 3 位小数后,3.1415926 四舍五入显示为 3.142。如果整段程序都统一保留 2 位小数,平时只需在开头写一次 std::cout << std::fixed << std::setprecision(2); 即可。
这里小伙伴们还要注意一点:格式化工具改变的只是屏幕上的 “输出显示效果”,变量 temperature 和 pi 在内存中保存的真实数值并没有发生任何改变。
2. 加与不加 std::fixed 的区别
如果我们省略前面的 std::fixed,只单独写 std::setprecision(n),此时会发生什么呢?
在默认情况下,单独写 std::setprecision(n) 控制的并不是 “小数点后保留几位”,而是整数部分和小数部分合起来的总数字个数,也就是有效数字的总位数。请看下面例子。
示例 2:加与不加 std::fixed
#include <iomanip>
#include <iostream>
int main() {
double number = 123.456789;
// 不加 std::fixed
std::cout << std::setprecision(4) << number << '\n';
// 加上 std::fixed
std::cout << std::fixed;
std::cout << std::setprecision(4) << number << '\n';
return 0;
}运行结果如下。
123.5
123.4568分析:
从运行结果可以看出,第一行输出了 123.5,第二行输出了 123.4568。第一处没有加 std::fixed,std::setprecision(4) 表示整数与小数加起来共保留 4 个有效数字,整数 123 已占 3 位,小数只剩 1 位,四舍五入后得到 123.5。
加上 std::fixed 之后,std::setprecision(4) 才表示 “小数点后保留 4 位”,输出为 123.4568。因此,日常只要需求是固定保留几位小数,直接把这两个工具搭配使用就可以了。
控制输出宽度与对齐
在打印商品清单、成绩单这类多列数据时,我们希望不同列的数据能够整齐地上下对齐,此时可以使用 std::setw()、std::left 和 std::right。
1. 设置输出宽度:std::setw()
在 C++ 中,我们可以使用 std::setw() 来指定紧接着下一项输出内容所占据的最小字符宽度,让多行数据按固定列宽排列。
语法:
std::cout << std::setw(n) << 输出内容;说明:
setw 是英文 “set width(设置宽度)” 的缩写,参数 n 表示输出结果至少占据 n 个字符的宽度。如果输出内容本身不满 n 个字符,默认采用右对齐方式,自动在内容左边补上空格。
示例 3:设置输出列宽
#include <iomanip>
#include <iostream>
int main() {
std::cout << std::setw(10) << "Apple";
std::cout << std::setw(8) << 12 << '\n';
std::cout << std::setw(10) << "Orange";
std::cout << std::setw(8) << 8 << '\n';
return 0;
}运行结果如下。
Apple 12
Orange 8分析:
std::setw(10) 将第一列的最小宽度设为 10 个字符,std::setw(8) 将第二列设为 8 个字符。以第一行为例,"Apple" 自身占 5 个字符,左边自动补 5 个空格。数字 12 占 2 个字符,左边自动补 6 个空格,两行数据默认都靠右对齐。
需要注意的是,std::setw(n) 设置的是 “最小宽度” 而不是最大宽度。如果输出内容本身的长度已经超过了 n,比如使用 std::setw(3) 去输出 5 个字符的 "Apple",程序依然会把 "Apple" 完整打印出来,不会截断多余的字符。
提示 如果希望宽度不足时不补空格而是补其他字符,比如把编号 7 补零显示为 007,可以配合 std::setfill('0') 使用,写成 std::cout << std::setfill('0') << std::setw(3) << 7; 即可。
2. 控制对齐方向:std::left 和 std::right
使用 std::setw(n) 设置宽度时,默认是靠右对齐的。如果我们希望文字靠左对齐,可以配合 std::left 和 std::right 手动指定对齐方向。
语法:
std::cout << std::left << std::setw(n) << 输出内容;
std::cout << std::right << std::setw(n) << 输出内容;说明:
从英文意思上就很容易记住:left 是 “左”,right 是 “右”。设置为 std::left 左对齐后,内容靠左显示,不够的宽度在右边补空格。设置为 std::right 右对齐后,内容靠右显示,不够的宽度在左边补空格。
示例 4:左对齐与右对齐排版
#include <iomanip>
#include <iostream>
int main() {
std::cout << std::left << std::setw(10) << "Name";
std::cout << std::right << std::setw(8) << "Score" << '\n';
std::cout << std::left << std::setw(10) << "Jack";
std::cout << std::right << std::setw(8) << 95 << '\n';
std::cout << std::left << std::setw(10) << "Lucy";
std::cout << std::right << std::setw(8) << 88 << '\n';
return 0;
}运行结果如下。
Name Score
Jack 95
Lucy 88分析:
这里第一列姓名使用了 std::left,文字在 10 个字符宽度内靠左对齐。第二列分数使用了 std::right,内容在 8 个字符宽度内靠右对齐。打印这种 “文字 + 数字” 的多列数据时,文字左对齐、数字右对齐是最清晰美观的排版方式。
另外留意一个小细节:第一行打印表头 "Score" 时,换行符 '\n' 是单独写在后面的,不要合并写成 std::setw(8) << "Score\n",否则 \n 也会占掉 1 个字符宽度,导致表头向左偏移 1 格。
格式控制的生效范围
前面介绍的这些格式控制工具,设置一次之后是一直有效,还是只对当前这一次输出有效呢?我们可以按生效范围将它们分为以下 2 类:
- 持续生效的工具:包括
std::fixed、std::setprecision()、std::left、std::right和std::setfill()。它们一旦设置,就会一直保持生效,继续影响后面的所有相关输出,直到被重新修改为止。 - 只对下一项生效的工具:只有
std::setw()。它是一个 “一次性” 工具,只对紧挨在它后面的那一项输出生效,一旦那一项打印完毕,宽度限制就会立即失效。
这一点初学者非常容易搞混,下面我们通过一个例子来直观对比两者的差异。
示例 5:格式工具的生效范围
#include <iomanip>
#include <iostream>
int main() {
// fixed 和 setprecision 持续生效
std::cout << std::fixed << std::setprecision(2);
std::cout << 3.1 << '\n';
std::cout << 5.678 << '\n';
// setw(4) 只对紧接着的 10 生效
std::cout << std::setfill('0');
std::cout << std::setw(4) << 10 << 20 << '\n';
// 在 10 和 20 前分别写 setw(4)
std::cout << std::setw(4) << 10 << ' ' << std::setw(4) << 20;
return 0;
}运行结果如下。
3.10
5.68
001020
0010 0020分析:
开头的 std::fixed 和 std::setprecision(2) 只设置了一次,后面的 3.1 和 5.678 就全都按保留 2 位小数的格式输出成了 3.10 和 5.68,说明这两个工具设置一次就会持续生效。
再对比最后两行输出,这里配合了持续生效的 std::setfill('0') 把空位补上 '0':倒数第二行只在 10 前面写了一次 std::setw(4),因此只有 10 补齐为 0010,后面的 20 宽度设置已失效,紧贴在后面变成了 001020。
而最后一行在 10 和 20 前面都写了 std::setw(4),两项数据才各自占满 4 个字符宽度。因此大家在对齐多列数据时,记得在每一项需要控制宽度的数据前面都单独写一次 std::setw()。
另外,如果前面设置过 std::setfill('0'),后面又想把填充字符改回默认的空格,只需再执行一句 std::cout << std::setfill(' '); 即可恢复。此外,现代 C++20 标准还新增了 std::format() 格式化函数,等打牢前面的基础语法后再去了解会更加轻松。
