C++ 控制台打印格式化技巧

下次您在控制台输出格式化方面遇到困难时,请参考本文或相关的速查表。
41 位读者喜欢这篇文章。
Woman sitting in front of her computer

Ray Smith

当我开始写作时,我主要是为了给自己做文档。在编程方面,我非常健忘,所以我开始记下有用的代码片段、特殊特性以及我使用的编程语言中的常见错误。本文完美地契合了最初的想法,因为它涵盖了从 C++ 打印到控制台时格式化的常见用例。

[下载 C++ std::cout 速查表]

像往常一样,本文附带了许多示例。除非另有说明,否则代码片段中显示的所有类型和类都是 std 命名空间的一部分。因此,在阅读此代码时,您必须在类型和类前面加上 using namespace std;。当然,示例代码也可以在 GitHub 上找到。

面向对象的流

如果您曾经用 C++ 编程,您肯定已经使用过 cout。当您包含 <iostream> 时,类型为 ostreamcout 对象将进入作用域。本文重点介绍 cout,它允许您打印到控制台,但此处描述的通用格式化对于类型为 ostream 的所有流对象均有效。ostream 对象是 basic_ostream 的实例,其模板参数的类型为 char。标头 <iosfwd> 是 <iostream> 的包含层次结构的一部分,其中包含常用类型的前向声明。

basic_ostream 继承自 basic_ios,而此类型又继承自 ios_base。在 cppreference.com 上,您可以找到一个类图,显示不同类之间的关系。

类 ios_base 是所有 I/O 流类的基类。类 basic_ios 是一个模板类,它具有针对称为 ios 的常用字符类型的特化。因此,当您在标准 I/O 的上下文中阅读有关 ios 的内容时,它是 basic_ios 的 char 类型特化。

格式化流

通常,有三种格式化基于 ostream 的流的方法

  1. 使用 ios_base 提供的格式标志。
  2. 标头 <iomanip> 和 <ios> 中定义的流修改函数。
  3. 通过调用 << 运算符的 特定重载

所有方法都有其优点和缺点,通常取决于使用哪种方法的情况。以下示例显示了所有方法的混合使用。

右对齐

默认情况下,cout 占用打印数据所需的空间。为了使右对齐输出生效,您必须定义一行允许占用的最大宽度。我使用格式标志来实现目标。

右对齐输出的标志和宽度调整仅适用于后续行

cout.setf(ios::right, ios::adjustfield);
cout.width(50);
cout << "This text is right justified" << endl;
cout << "This text is left justified again" << endl;

在上面的代码中,我使用 setf 配置右对齐输出。我建议您将位掩码 ios::adjustfield 应用于 setf,这会导致位掩码指定的所有标志在实际 ios::right 标志设置之前重置,以防止冲突的组合。

填充空白

使用右对齐输出时,空白默认填充空格。您可以使用 setfill 指定填充字符来更改它

cout << right << setfill('.') << setw(30) << 500 << " pcs" << endl;
cout << right << setfill('.') << setw(30) << 3000 << " pcs" << endl;
cout << right << setfill('.') << setw(30) << 24500 << " pcs" << endl;

代码产生以下输出

...........................500 pcs
..........................3000 pcs
.........................24500 pcs

组合

想象一下,您的 C++ 程序跟踪您的食品储藏室库存。您时不时地想打印当前库存清单。为此,您可以使用以下格式。

以下代码是左对齐和右对齐输出的组合,使用点作为填充字符以获得美观的列表

cout << left << setfill('.') << setw(20) << "Flour" << right << setfill('.') << setw(20) << 0.7 << " kg" << endl;
cout << left << setfill('.') << setw(20) << "Honey" << right << setfill('.') << setw(20) << 2 << " Glasses" << endl;
cout << left << setfill('.') << setw(20) << "Noodles" << right << setfill('.') << setw(20) << 800 << " g" << endl;
cout << left << setfill('.') << setw(20) << "Beer" << right << setfill('.') << setw(20) << 20 << " Bottles" << endl;

输出

Flour...............................0.70 kg
Honey..................................2 Glasses
Noodles..............................800 g
Beer..................................20 Bottles

打印值

当然,基于流的输出还提供了多种可能性来输出各种变量类型。

布尔值

boolalpha 开关允许您将布尔值的二进制解释转换为字符串

cout << "Boolean output without using boolalpha: " << true << " / " << false << endl;
cout << "Boolean output using boolalpha: " << boolalpha << true << " / " << false << endl;

上面的行产生以下输出

Boolean output without using boolalpha: 1 / 0
Boolean output using boolalpha: true / false

地址

如果整数的值应被视为地址,则将其强制转换为 void* 以调用正确的重载就足够了。这是一个例子

unsigned long someAddress = 0x0000ABCD;
cout << "Treat as unsigned long: " << someAddress << endl;
cout << "Treat as address: " << (void*)someAddress << endl;

代码产生以下输出

Treat as unsigned long: 43981
Treat as address: 0000ABCD

代码以正确的长度打印地址。32 位可执行文件产生上述输出。

整数

打印整数很简单。出于演示目的,我使用 setf 和 setiosflags 指定数字的基数。应用流修饰符 hex/oct 将具有相同的效果

int myInt = 123;

cout << "Decimal: " << myInt << endl;

cout.setf(ios::hex, ios::basefield);
cout << "Hexadecimal: " << myInt << endl;

cout << "Octal: " << resetiosflags(ios::basefield) <<  setiosflags(ios::oct) << myInt << endl;

注意: 默认情况下,没有用于已用基数的指示器,但您可以使用 showbase 添加一个。

Decimal: 123
Hexadecimal: 7b
Octal: 173

用零填充

0000003
0000035
0000357
0003579

您可以通过指定宽度和填充字符来获得如上所示的输出

cout << setfill('0') << setw(7) << 3 << endl;
cout << setfill('0') << setw(7) << 35 << endl;
cout << setfill('0') << setw(7) << 357 << endl;
cout << setfill('0') << setw(7) << 3579 << endl;

浮点值

如果我想打印浮点值,我可以在 fixedscientific 格式之间进行选择。此外,我可以指定精度。

double myFloat = 1234.123456789012345;
int defaultPrecision = cout.precision(); // == 2

cout << "Default precision: " << myFloat << endl;
cout.precision(4);
cout << "Modified precision: " << myFloat << endl;
cout.setf(ios::scientific, ios::floatfield);
cout << "Modified precision & scientific format: " << myFloat << endl;
/* back to default */
cout.precision(defaultPrecision);
cout.setf(ios::fixed, ios::floatfield);
cout << "Default precision & fixed format:  " << myFloat << endl;

上面的代码产生以下输出

Default precision: 1234.12
Modified precision: 1234.1235
Modified precision & scientific format: 1.2341e+03
Default precision & fixed format:  1234.12

时间和金钱

使用 put_money,您可以以正确的、依赖于区域设置的格式打印货币单位。这要求您的控制台可以输出 UTF-8 字符集。请注意,变量 specialOffering 以美分存储货币价值

long double specialOffering = 9995;

cout.imbue(locale("en_US.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
cout.imbue(locale("de_DE.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
cout.imbue(locale("ru_RU.UTF-8"));
cout  << showbase << put_money(specialOffering) << endl;

ios 的 imbue 方法允许您指定区域设置。使用命令 locale -a,您可以获得系统上所有可用区域设置标识符的列表。

$99.95
99,950€
99,950₽

(出于某种原因,它在我的系统上以三位小数位打印欧元和卢布,这对我来说看起来很奇怪,但这可能是官方格式。)

相同的原则适用于时间输出。函数 put_time 允许您以相应的区域设置格式打印时间。此外,您可以指定要打印的时间对象的哪些部分。

time_t now = time(nullptr);
tm localtm = *localtime(&now);


cout.imbue(locale("en_US.UTF-8"));
cout << "en_US : " << put_time(&localtm, "%c") << endl;
cout.imbue(locale("de_DE.UTF-8"));
cout << "de_DE : " << put_time(&localtm, "%c") << endl;
cout.imbue(locale("ru_RU.UTF-8"));
cout << "ru_RU : " << put_time(&localtm, "%c") << endl;

格式说明符 %c 导致打印标准日期和时间字符串

en_US : Tue 02 Nov 2021 07:36:36 AM CET
de_DE : Di 02 Nov 2021 07:36:36 CET
ru_RU : Вт 02 ноя 2021 07:36:36

创建自定义流修饰符

您还可以创建自己的流。以下代码在应用于 ostream 对象时插入预定义的字符串

ostream& myManipulator(ostream& os) {
    string myStr = ">>>Here I am<<<";
    os << myStr;
    return os;
}

另一个例子: 如果您有重要的事情要说,就像互联网上的大多数人一样,您可以使用以下代码在您的消息后插入感叹号,具体取决于重要性级别。重要性级别作为参数传递

struct T_Importance {
     int levelOfSignificance;
};

T_Importance importance(int lvl){ 
    T_Importance x = {.levelOfSignificance = lvl };
    return x;
}

ostream& operator<<(ostream& __os, T_Importance t){

    for(int i = 0; i < t.levelOfSignificance; ++i){
        __os.put('!');
    }
    return __os;
}

现在可以将这两个修饰符简单地传递给 cout

cout << "My custom manipulator: " << myManipulator << endl;

cout << "I have something important to say" << importance(5) << endl;

产生以下输出

My custom manipulator: >>>Here I am<<<

I have something important to say!!!!!

结论

下次您在控制台输出格式化方面遇到困难时,我希望您能记住本文或相关的 速查表

在 C++ 应用程序中,coutprintf 的新邻居。虽然使用 printf 仍然有效,但我可能总是更喜欢使用 cout。特别是与 <ios> 中定义的修改函数结合使用,可以产生美观、可读的代码。

标签
User profile image.
Stephan 是一位技术爱好者,他欣赏开源,因为它能深入了解事物的工作原理。Stephan 在工业自动化软件这个主要为专有领域的公司担任全职支持工程师。如果可能,他会致力于他基于 Python 的开源项目、撰写文章或骑摩托车。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.