第 06 章

第 6 章:输出格式化、数值范围与数学函数

本章对应长视频 5:55:57–7:03:15。系统讲解 iomanip 的流操纵器:宽度、对齐、填充、进制、浮点格式与精度;再用 limits 查询类型范围、用 cmath 做数学运算,最后解释小于 4 字节的整型在运算时会被提升为 int。

Chapter 6: Output Formatting, Type Ranges and Math

Covers 5:55:57–7:03:15 of the long video. It treats the iomanip manipulators systematically — width, alignment, fill, bases, floating-point format and precision — then queries type ranges with limits and does arithmetic with cmath, and ends with the integral promotion of types narrower than int.

首发于 2022.02.17中 / EN
系统课程cppiomanipformattingnumeric-limitscmathintegral-promotion
语言
字号
目录
篇内目录

这一章在讲什么

第 6 章覆盖 5:55:57–7:03:15,是前半程篇幅最长的一章。它处理三件事:

  • 把输出排整齐:宽度、左右对齐、填充字符、进制、浮点格式、精度。
  • 查类型的边界:<limits> 提供 min()、max()、lowest() 等函数。
  • 做数学运算:<cmath> 提供取整、绝对值、指数、对数、幂、开方、三角函数。

章末还有一个容易被忽略但很关键的知识点:小于 4 字节的整型参与运算时,会先被隐式提升为 int。

时间轴

时间内容
5:55:57为什么要格式化:把杂乱数据变成表格
5:58:34endl 与 '\n';flush 与输出缓冲区
6:00:05setw 设置宽度
6:00:56left / right / internal 对齐
6:01:41setfill 填充字符
6:02:02boolalpha、showpos
6:03:15dec / hex / oct、showbase、uppercase
6:06:19fixed / scientific 与浮点默认格式
6:08:16setprecision 精度;showpoint
6:33:24numeric_limits:min、max、lowest
6:41:09cmath:floor、ceil、abs、exp、pow、log、sqrt、round
6:54:21小于 4 字节的整型与整型提升

流操纵器从哪来

格式化工具来自两个头文件:

#include <iostream>   // cout、cerr、endl
#include <iomanip>    // setw、setfill、setprecision 等

不用背:查 cppreference 上 std::setw 的页面,第一行就写着它需要 <iomanip>。课程反复强调的一点是——学会查文档,比记住每个操纵器属于哪个头文件更重要。

endl 与 '\n'

std::cout << "Hello" << std::endl << "World" << std::endl;
std::cout << "Hello" << '\n' << "World" << '\n';

两者在终端里看起来一样,但 std::endl 除了换行还会刷新缓冲区,'\n' 只写一个换行符。频繁输出时,endl 的刷新是有成本的;不过现在就把它俩理解为"都能换行"即可,差别属于性能细节。

flush 与缓冲区

std::cout 的输出不是立刻送到终端,而是先写进一块缓冲区,等缓冲区满了或遇到刷新时才一次性送出。std::flush 强制立即送出:

std::cout << "正在计算..." << std::flush;

在需要"立刻看到输出"的场景(进度提示、调试)里有用;普通程序交给缓冲区自动处理即可。

排版:宽度、对齐、填充

未格式化的表格:

std::cout << "Daniel" << "Gray" << 33 << std::endl;
std::cout << "Woods" << "Smith" << 27 << std::endl;

数据挤在一起,难以阅读。用 setw 给每列指定宽度:

#include <iomanip>

int width {10};

std::cout << std::setw(width) << "Lastname"
          << std::setw(width) << "Firstname"
          << std::setw(width) << "Age" << std::endl;

std::cout << std::setw(width) << "Daniel"
          << std::setw(width) << "Gray"
          << std::setw(width) << 33 << std::endl;

setw(10) 表示"下一个输出至少占 10 个字符宽",不足的用空格补齐。

三个必须知道的细节:

  1. setw 只对下一次输出生效,每一列都要重新写。
  2. 默认右对齐:数据靠右,空格在左。
  3. 宽度可以存进变量,改一处就能调整整张表,比在每行写死 10 好得多。

对齐方式

std::cout << std::right << std::setw(10) << "Gray";
std::cout << std::left  << std::setw(10) << "Gray";

std::left、std::right 是持续生效的设置,一旦设置,后面所有输出都按这个方式对齐,直到再次改变。

internal:符号靠左,数值靠右

std::cout << std::internal << std::setw(10) << -12.34 << std::endl;

输出形如 - 12.34:负号贴左,数字贴右。做金额表、对齐带符号的数列时很实用。

填充字符

std::cout << std::left << std::setfill('-') << std::setw(10) << "Daniel";

空格被换成 -。setfill 同样持续生效,setw 仍然只管一次。

布尔、符号与进制

boolalpha

bool condition {true};
std::cout << condition << std::endl;                    // 1
std::cout << std::boolalpha << condition << std::endl;  // true
std::cout << std::noboolalpha << condition << std::endl; // 1

showpos

std::cout << 34 << " " << -45 << std::endl;                // 34 -45
std::cout << std::showpos << 34 << " " << -45 << std::endl; // +34 -45

正数默认不显示 +,showpos 强制显示;负数无论是否开启都带 -。

进制

int number {42};
std::cout << std::dec << number << std::endl;   // 42
std::cout << std::hex << number << std::endl;   // 2a
std::cout << std::oct << number << std::endl;   // 52

进制设置只影响整数,浮点数会照原样输出。

showbase 与 uppercase

std::cout << std::showbase;
std::cout << std::hex << 42 << std::endl;   // 0x2a
std::cout << std::oct << 42 << std::endl;   // 052

std::cout << std::uppercase << std::hex << 42 << std::endl;  // 0X2A

showbase 加上前缀:十六进制 0x、八进制 0、十进制不加。uppercase 让 a–f 变成 A–F,前缀也变成 0X。

注意: 进制设置会一直生效。切到 hex 之后忘了切回 dec,后面所有整数都会以十六进制打印。这是初学者排查"数字怎么变成字母了"的常见原因。

浮点格式与精度

默认、fixed 与 scientific

double a {3.14159265358979};
double b {1.0};
double c {1.34e-10};

std::cout << a << " " << b << " " << c << std::endl;
// 3.14159 1 1.34e-10

std::cout << std::fixed << a << std::endl;        // 3.141593
std::cout << std::scientific << a << std::endl;   // 3.141593e+00

默认格式会在必要时自动使用科学计数法;fixed 强制定点,scientific 强制科学计数法。

恢复到默认

标准库没有提供一个"恢复浮点默认格式"的操纵器。课程给出的写法是:

std::cout.unsetf(std::ios::floatfield);   // 清掉 fixed / scientific
std::cout.setf(std::ios::fmtflags(0), std::ios::floatfield);

这属于工程上的变通做法,现阶段照用即可,不必深究。

setprecision

std::cout << std::setprecision(6)  << a << std::endl;
std::cout << std::setprecision(10) << a << std::endl;
std::cout << std::setprecision(20) << a << std::endl;

精度设置"显示多少位",但不能突破类型本身的精度。double 大约 15–17 位,超过这个数之后的数字是噪声:

double value {1.234567890123456789};
std::cout << std::setprecision(20) << value << std::endl;
// 1.2345678901234567 后面跟的是垃圾

在 fixed 模式下,setprecision 表示小数点后的位数;在默认/科学计数法下,它表示有效数字总位数。这个差别很容易踩坑。

showpoint

std::cout << 12.0 << std::endl;                  // 12
std::cout << std::showpoint << 12.0 << std::endl; // 12.0000

showpoint 强制显示小数点,并补足尾随的 0。

查询类型范围:numeric_limits

#include <limits>

std::cout << std::numeric_limits<short>::min() << std::endl;   // -32768
std::cout << std::numeric_limits<short>::max() << std::endl;   // 32767

std::cout << std::numeric_limits<float>::min() << std::endl;   // 最小正数
std::cout << std::numeric_limits<float>::lowest() << std::endl; // 最负的数
std::cout << std::numeric_limits<float>::max() << std::endl;   // 最大数

关键区别:

  • 对整数,min() 就是最小值(负数),lowest() 与它相同。
  • 对浮点数,min() 是最小的正数(靠近 0),lowest() 才是最负的数。名字容易误解,这是必须记住的一点。

还可以查 is_signed、digits 等属性,在需要判断平台特性时很有用。

数学函数:cmath

#include <cmath>

std::floor(7.7);    // 7  向下取整
std::ceil(7.2);     // 8  向上取整
std::abs(-5000);    // 5000 绝对值
std::exp(10);       // e^10 ≈ 22026.5
std::pow(3, 4);     // 81
std::log(54.59);    // 以 e 为底
std::log10(10000);  // 4
std::sqrt(81);      // 9
std::round(2.5);    // 3  四舍五入,远离零
std::round(2.4);    // 2

几个容易出错的地方:

  • std::log 默认以 ee 为底,不是以 10 为底;常用对数要写 std::log10。
  • std::round 对 .5 的处理是"远离零":2.5 → 3,-2.5 → -3。
  • 三角函数用弧度,不是角度。

<cmath> 覆盖的函数远不止这些,需要时查 cppreference 即可。

小于 4 字节的整型与整型提升

C++ 规定:小于 int 的整型(char、short)参与算术运算时,会先被隐式转换为 int。 原因是 CPU 的整数运算单元以 int 为最小宽度,这样处理最自然。

short a {10};
short b {20};

auto result = a + b;
std::cout << sizeof(a) << std::endl;       // 2
std::cout << sizeof(b) << std::endl;       // 2
std::cout << sizeof(result) << std::endl;  // 4,不是 2

a + b 在计算前先把两个 short 提升为 int,相加的结果也是 int,所以 auto 推导出 4 字节的 int。char 同理:

char c1 {'a'};
char c2 {'b'};
auto result2 = c1 + c2;    // int,值是 195('a' 的 97 加 'b' 的 98)

这条规则平时不太显眼,但当程序依赖 sizeof 或类型宽度(序列化、二进制协议、模板特化)时,它会带来真实的问题。

本章结论

  • 格式化需要 <iomanip>;setw 只影响下一次输出,left/right/internal/setfill 持续生效。
  • boolalpha 控制布尔显示,showpos 控制正号。
  • dec/hex/oct 只影响整数,且会持续生效;showbase 加前缀,uppercase 转大写。
  • 浮点格式有默认、fixed、scientific;setprecision 在 fixed 下是小数位数,否则是有效数字位数。
  • numeric_limits::min() 对浮点数是"最小正数",lowest() 才是"最负数"。
  • <cmath> 提供取整、绝对值、指数、对数、幂、开方与三角函数;log 以 ee 为底。
  • 小于 4 字节的整型在运算时提升为 int,结果的类型可能比操作数更宽。

理解检验

1. 为什么 setw(10) 写一次不够,而 std::left 写一次就够?

参考答案:setw 是"只对下一次输出生效"的操纵器,它设置的是紧随其后的那一个值的宽度,输出完就失效,所以每一列都要重新写。left/right/internal/setfill 修改的是流的持久格式化状态,一旦设置,之后所有输出都沿用,直到再次改变。前者是逐次参数,后者是开关。

2. 执行 std::cout << std::hex << 42; 之后再输出 100,会看到什么?

参考答案:会看到 64。hex 设置的是流的进制状态,会持续生效;100 被解释为十六进制输出,100÷16=6100 \div 16 = 6 余 4,即 64。要恢复正常需要显式写 std::dec。这是"格式化状态会残留"的典型例子。

3. numeric_limits<float>::min() 与 lowest() 有什么区别?为什么容易误解?

参考答案:对浮点类型,min() 返回最小的正规格化正数(最接近 0 的正值,约为 1.18×10−381.18\times10^{-38}),lowest() 返回最负的有限值(约为 −3.4×1038-3.4\times10^{38})。名字里的 "min" 容易被理解成"最小值",但在浮点语境下它指的是"最小正值"。整数类型没有这个问题,因为整数的 min() 与 lowest() 相同。

4. std::setprecision(20) 能让我们看到 double 的 20 位有效数字吗?

参考答案:不能。setprecision 只规定显示多少位,实际精度受类型限制:double 约 15–17 位有效数字,超过这个范围后输出的数字没有意义,只是二进制表示转换后的噪声。float 更早开始失真,大约 7 位。想真正提高精度要用 long double(也有限)或专门的高精度库。

5. short a = 10, b = 20; auto c = a + b; 中 c 是什么类型?为什么?

参考答案:c 是 int(4 字节)。C++ 的整型提升规则要求:宽度小于 int 的整型在参与算术运算前先转换为 int。a 和 b 先各自变成 int,相加的结果也是 int,auto 据此推导。用 sizeof(c) 可以验证结果是 4 而不是 2。

6. 想让 3.14159 以固定三位小数输出、并且正数带 + 号,应该怎样设置?

参考答案:

std::cout << std::fixed << std::setprecision(3) << std::showpos;
std::cout << 3.14159 << std::endl;   // +3.142

fixed 让 setprecision(3) 表示小数点后三位,showpos 让正数显示 +。注意输出的 3.142 是四舍五入的结果,不是截断。这三个设置都会持续生效,后续输出会一直带 + 和三位小数,需要时用 std::noshowpos、恢复默认格式来复位。

与前后章的关系

本章把"输出"从一个语句变成了一套可配置的排版系统,同时补齐了查类型范围与做数学运算的工具。第 7 章进入流程控制:if、else if、switch,用第 5 章的比较与逻辑结果决定程序走哪条分支。第 8 章再讲循环,把重复劳动交给 CPU。

What this chapter covers

Chapter 6 spans 5:55:57–7:03:15 and is the longest so far in the first half of the course. Three subjects:

  • Laying output out properly: width, alignment, fill characters, bases, floating-point format, precision.
  • Finding a type's limits: <limits> supplies min(), max(), lowest() and friends.
  • Doing mathematics: <cmath> supplies rounding, absolute value, exponential, logarithm, powers, roots and trigonometry.

It ends with an easily overlooked but important fact: integer types narrower than four bytes are implicitly promoted to int before arithmetic.

Timeline

TimeTopic
5:55:57Why formatting: turning loose data into a table
5:58:34endl versus '\n'; flush and the output buffer
6:00:05setw for width
6:00:56left / right / internal
6:01:41setfill
6:02:02boolalpha, showpos
6:03:15dec / hex / oct, showbase, uppercase
6:06:19fixed / scientific and the default float format
6:08:16setprecision; showpoint
6:33:24numeric_limits: min, max, lowest
6:41:09cmath: floor, ceil, abs, exp, pow, log, sqrt, round
6:54:21Types narrower than four bytes and integral promotion

Where the manipulators come from

Formatting lives in two headers:

#include <iostream>   // cout, cerr, endl
#include <iomanip>    // setw, setfill, setprecision, ...

No memorisation required: cppreference's page for std::setw names <iomanip> on its first line. The course keeps repeating one lesson — learn to read the documentation; knowing which header holds each manipulator is less valuable than knowing where to look it up.

endl versus '\n'

std::cout << "Hello" << std::endl << "World" << std::endl;
std::cout << "Hello" << '\n' << "World" << '\n';

On a terminal they look identical, but std::endl also flushes the buffer while '\n' only writes a newline. In tight output loops that flush costs something; for now the useful fact is simply that both break the line.

flush and the buffer

std::cout does not send every character straight to the terminal. Output is collected in a buffer and sent when it fills up or is flushed. std::flush forces the send:

std::cout << "Computing..." << std::flush;

Useful when the user must see progress immediately; ordinary programs can let the buffer do its job.

Layout: width, alignment, fill

An unformatted table:

std::cout << "Daniel" << "Gray" << 33 << std::endl;
std::cout << "Woods" << "Smith" << 27 << std::endl;

The data is jammed together. setw gives each column a width:

#include <iomanip>

int width {10};

std::cout << std::setw(width) << "Lastname"
          << std::setw(width) << "Firstname"
          << std::setw(width) << "Age" << std::endl;

std::cout << std::setw(width) << "Daniel"
          << std::setw(width) << "Gray"
          << std::setw(width) << 33 << std::endl;

Three details matter:

  1. setw applies only to the next output, so every column needs its own call.
  2. Right alignment is the default: data sits at the right, padding at the left.
  3. The width can live in a variable, so one edit adjusts the whole table instead of editing every line.

Alignment

std::cout << std::right << std::setw(10) << "Gray";
std::cout << std::left  << std::setw(10) << "Gray";

std::left and std::right are sticky: they apply to all subsequent output until changed.

internal: sign left, digits right

std::cout << std::internal << std::setw(10) << -12.34 << std::endl;

Produces - 12.34: the minus sign hugs the left edge, the digits the right. Handy for ledgers and aligned signed columns.

Fill characters

std::cout << std::left << std::setfill('-') << std::setw(10) << "Daniel";

Spaces become dashes. setfill is sticky; setw still lasts one output only.

Booleans, signs and bases

boolalpha

bool condition {true};
std::cout << condition << std::endl;                     // 1
std::cout << std::boolalpha << condition << std::endl;   // true
std::cout << std::noboolalpha << condition << std::endl; // 1

showpos

std::cout << 34 << " " << -45 << std::endl;                 // 34 -45
std::cout << std::showpos << 34 << " " << -45 << std::endl; // +34 -45

Positive numbers hide their sign by default; showpos forces it. Negative numbers always show -.

Bases

int number {42};
std::cout << std::dec << number << std::endl;   // 42
std::cout << std::hex << number << std::endl;   // 2a
std::cout << std::oct << number << std::endl;   // 52

The base affects integers only; floating-point values ignore it.

showbase and uppercase

std::cout << std::showbase;
std::cout << std::hex << 42 << std::endl;   // 0x2a
std::cout << std::oct << 42 << std::endl;   // 052

std::cout << std::uppercase << std::hex << 42 << std::endl;  // 0X2A

showbase adds the prefix: 0x for hex, 0 for octal, nothing for decimal. uppercase turns a–f into A–F, and 0x into 0X.

Warning: the base setting is sticky. Switch to hex, forget to switch back, and every later integer prints in hex. This is a common reason for "why did my numbers turn into letters?"

Floating-point format and precision

Default, fixed and scientific

double a {3.14159265358979};
double b {1.0};
double c {1.34e-10};

std::cout << a << " " << b << " " << c << std::endl;
// 3.14159 1 1.34e-10

std::cout << std::fixed << a << std::endl;        // 3.141593
std::cout << std::scientific << a << std::endl;   // 3.141593e+00

The default format uses scientific notation when it needs to; fixed forces decimal notation, scientific forces exponent notation.

Restoring the default

The standard library offers no manipulator for "go back to the default float format". The course's workaround:

std::cout.unsetf(std::ios::floatfield);
std::cout.setf(std::ios::fmtflags(0), std::ios::floatfield);

This is an engineering workaround; copy it for now, the explanation is out of scope here.

setprecision

std::cout << std::setprecision(6)  << a << std::endl;
std::cout << std::setprecision(10) << a << std::endl;
std::cout << std::setprecision(20) << a << std::endl;

Precision controls how many digits are displayed, and cannot exceed the type's own precision. A double holds roughly 15–17 significant digits; anything beyond that is noise:

double value {1.234567890123456789};
std::cout << std::setprecision(20) << value << std::endl;
// 1.2345678901234567 followed by garbage

In fixed mode setprecision means digits after the decimal point; in default or scientific mode it means total significant digits. That difference is a real trap.

showpoint

std::cout << 12.0 << std::endl;                   // 12
std::cout << std::showpoint << 12.0 << std::endl; // 12.0000

showpoint forces the decimal point and pads trailing zeros.

Querying type ranges: numeric_limits

#include <limits>

std::cout << std::numeric_limits<short>::min() << std::endl;   // -32768
std::cout << std::numeric_limits<short>::max() << std::endl;   // 32767

std::cout << std::numeric_limits<float>::min() << std::endl;    // smallest positive
std::cout << std::numeric_limits<float>::lowest() << std::endl; // most negative
std::cout << std::numeric_limits<float>::max() << std::endl;    // largest

The distinction to remember:

  • For integers, min() is the smallest value (negative), and lowest() equals it.
  • For floating point, min() is the smallest positive value (closest to zero), and lowest() is the most negative. The naming misleads almost everyone the first time.

Other members such as is_signed and digits help when code must adapt to the platform.

Math functions: cmath

#include <cmath>

std::floor(7.7);    // 7    round down
std::ceil(7.2);     // 8    round up
std::abs(-5000);    // 5000 absolute value
std::exp(10);       // e^10 ≈ 22026.5
std::pow(3, 4);     // 81
std::log(54.59);    // base e
std::log10(10000);  // 4
std::sqrt(81);      // 9
std::round(2.5);    // 3    halfway away from zero
std::round(2.4);    // 2

Three easy mistakes:

  • std::log is base ee, not base 10; for base 10 use std::log10.
  • std::round sends halfway values away from zero: 2.5 → 3, -2.5 → -3.
  • Trigonometry takes radians, not degrees.

<cmath> has far more than this list; consult cppreference when needed.

Types narrower than four bytes and integral promotion

The standard says: integer types narrower than int (char, short) are implicitly converted to int before arithmetic. The reason is hardware — the CPU's integer unit works natively at int width.

short a {10};
short b {20};

auto result = a + b;
std::cout << sizeof(a) << std::endl;       // 2
std::cout << sizeof(b) << std::endl;       // 2
std::cout << sizeof(result) << std::endl;  // 4, not 2

The addition promotes both shorts to int first, and the sum is an int, so auto deduces a four-byte int. The same happens with char:

char c1 {'a'};
char c2 {'b'};
auto result2 = c1 + c2;    // int, value 195 ('a' 97 + 'b' 98)

The rule is invisible most of the time, but it bites whenever code depends on sizeof or exact widths — serialisation, binary protocols, template specialisation.

Chapter summary

  • Formatting needs <iomanip>; setw affects the next output only, while left/right/internal/setfill persist.
  • boolalpha controls boolean display, showpos controls the plus sign.
  • dec/hex/oct affect integers only and persist; showbase adds prefixes, uppercase uppercases.
  • Float formats are default, fixed and scientific; setprecision means decimals in fixed mode, significant digits otherwise.
  • numeric_limits::min() is the smallest positive float; lowest() is the most negative.
  • <cmath> supplies rounding, absolute value, exponential, logarithm, powers, roots and trigonometry; log is base ee.
  • Types narrower than four bytes are promoted to int, so the result type can be wider than the operands.

Comprehension check

1. Why is one setw(10) not enough while one std::left is?

Reference answer: setw is a one-shot manipulator: it sets the field width for the single value that follows and then expires, so every column needs its own call. left/right/internal/setfill modify the stream's persistent formatting state; once set they apply to all later output until changed. The first is a per-item argument, the second a switch.

2. After std::cout << std::hex << 42;, what does printing 100 show?

Reference answer: 64. hex sets a persistent stream state, so 100 is interpreted and printed in hexadecimal: 100÷16=6100 \div 16 = 6 remainder 4, giving 64. Restoring decimal output requires an explicit std::dec. This is the classic case of formatting state leaking into later output.

3. What is the difference between numeric_limits<float>::min() and lowest(), and why is it confusing?

Reference answer: For floating-point types min() returns the smallest positive normalised value (closest to zero, about 1.18×10−381.18\times10^{-38}), while lowest() returns the most negative finite value (about −3.4×1038-3.4\times10^{38}). The English word "min" suggests "smallest value", but in floating-point context it means "smallest positive". Integer types do not have this confusion: min() and lowest() coincide.

4. Can std::setprecision(20) show 20 significant digits of a double?

Reference answer: No. setprecision requests a display width, but the type's own precision limits the real information: double carries roughly 15–17 significant digits, and digits beyond that are conversion noise, not data. float degrades from about 7 digits. Genuinely more precision requires long double (still finite) or an arbitrary-precision library.

5. What is the type of c in short a = 10, b = 20; auto c = a + b; and why?

Reference answer: c is an int (4 bytes). The integral promotion rule converts any type narrower than int to int before arithmetic, so both shorts are promoted, the sum is an int, and auto deduces accordingly. sizeof(c) confirms 4 rather than 2.

6. How do you print 3.14159 with three decimals and a leading plus sign for positive values?

Reference answer:

std::cout << std::fixed << std::setprecision(3) << std::showpos;
std::cout << 3.14159 << std::endl;   // +3.142

fixed makes setprecision(3) mean three digits after the point, and showpos forces the plus sign. The output 3.142 is rounded, not truncated. All three settings persist, so later output keeps the plus and three decimals until std::noshowpos and a format reset undo them.

How it connects

This chapter turns output from a statement into a configurable layout system, and adds the tools for querying ranges and doing mathematics. Chapter 7 enters control flow — if, else if, switch — using the comparisons and logical results from chapter 5 to choose a branch. Chapter 8 then covers loops, handing repetition to the CPU.

来源