Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

格式化输出 (format)

C++20 引入的格式化库,用 std::format 取代 printf 风格与流风格的输出;C++23 增加了 std::print。

它解决了两边的痛点:printf 类型不安全、不可扩展;iostream 冗长、状态易被污染、性能一般。std::format 借鉴了 Python 的 str.format 和 {fmt} 库的设计,兼顾安全性和可读性。

1. 引入

#include <format>   // C++20:std::format
#include <print>    // C++23:std::print

2. 核心概念

1. 为什么不用 printf 和 iostream

printf 的问题:

printf("%d\n", 3.14);   // 类型不匹配,未定义行为,编译器通常不报错
printf("%s\n", 42);     // 同上

格式串和参数类型不匹配是经典 bug,printf 无法在编译期检查(除非编译器开了 -Wformat,但只覆盖部分情况)。而且 printf 无法直接输出自定义类型。

iostream 的问题:

std::cout << "x = " << x << ", y = " << y << "\n";

冗长,格式和内容混在一起,而且 std::hex、std::setprecision 这类操纵符会改变流的状态,忘记恢复会影响后续输出。

std::format 的写法:

std::string s = std::format("x = {}, y = {}", x, y);

类型安全(编译期检查)、简洁、不改变任何全局状态。

2. 基本形式

std::format(格式串, 参数...)

格式串里用 {} 作为占位符,按顺序对应参数:

std::string s = std::format("{} + {} = {}", 1, 2, 3);
// "1 + 2 = 3"

也可以指定位置:

std::format("{1} {0}", "world", "hello");   // "hello world"

3. 格式说明符

完整语法是 {[索引]:[填充][对齐][符号][#][0][宽度][.精度][类型]}。

常用的部分:

说明符含义示例
{}默认格式std::format("{}", 42) → "42"
{:d}十进制整数std::format("{:d}", 42) → "42"
{:x} / {:X}十六进制(小写/大写)std::format("{:x}", 255) → "ff"
{:o}八进制std::format("{:o}", 8) → "10"
{:b}二进制std::format("{:b}", 5) → "101"
{:f}定点浮点std::format("{:f}", 3.14) → "3.140000"
{:e}科学计数法std::format("{:e}", 314.0) → "3.140000e+02"
{:g}自动选择std::format("{:g}", 3.14) → "3.14"
{:c}字符std::format("{:c}", 65) → "A"
{:s}字符串std::format("{:s}", "hi") → "hi"
{:?}调试格式(C++23)带引号的字符串

3. 用法

1. 宽度、对齐与填充

std::format("{:10}", "hi");      // "hi        "(左对齐,宽度 10)
std::format("{:>10}", "hi");     // "        hi"(右对齐)
std::format("{:^10}", "hi");     // "    hi    "(居中)
std::format("{:*^10}", "hi");    // "****hi****"(用 * 填充)
std::format("{:010}", 42);       // "0000000042"(补零)

数字默认右对齐,字符串默认左对齐。

2. 精度

std::format("{:.2f}", 3.14159);   // "3.14"
std::format("{:.3e}", 31415.9);   // "3.142e+04"
std::format("{:.5}", "hello world");   // "hello"(截断字符串)

3. 符号与进制前缀

std::format("{:+d}", 42);      // "+42"
std::format("{: d}", 42);      // " 42"
std::format("{:#x}", 255);     // "0xff"
std::format("{:#b}", 5);       // "0b101"

4. 输出到流或文件

std::format 返回字符串,可以直接输出:

std::cout << std::format("x = {}\n", x);

std::ofstream out("log.txt");
out << std::format("[{}] {}\n", level, message);

C++23 的 std::print 更直接:

std::print("x = {}\n", x);              // 输出到 stdout
std::println("x = {}", x);              // 自动加换行
std::print(stderr, "错误: {}\n", msg);  // 输出到 stderr

print 通常比 cout 快,因为它不需要与 C 的 stdio 同步。

5. 格式化自定义类型

需要特化 std::formatter:

struct Point
{
    int x, y;
};

template<>
struct std::formatter<Point>
{
    constexpr auto parse(std::format_parse_context& ctx)
    {
        return ctx.begin();   // 不支持额外格式说明符
    }

    auto format(const Point& p, std::format_context& ctx) const
    {
        return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
    }
};

Point p{1, 2};
std::cout << std::format("{}", p);   // "(1, 2)"

如果要支持格式说明符(比如 {:.2f}),parse 需要解析它们并保存下来。

6. 格式化时间

<chrono> 类型可以直接格式化:

#include <chrono>
#include <format>

auto now = std::chrono::system_clock::now();
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", now);

7. 写入已有缓冲区

std::format_to 直接写入输出迭代器,避免中间字符串:

std::string buffer;
std::format_to(std::back_inserter(buffer), "x = {}", 42);

// 或者写入固定大小的缓冲区
char buf[100];
auto result = std::format_to_n(buf, sizeof(buf), "x = {}", 42);
// result.out 指向写入末尾

4. 注意事项

编译期检查。格式串是编译期常量时,std::format 会检查参数数量和类型:

std::format("{} {}", 1);        // 编译错误:参数太少
std::format("{:d}", "hello");   // 编译错误:字符串不能用 :d

这是相对 printf 的最大优势。但如果格式串是运行时字符串,检查就退化为运行时异常。

需要编译器支持。GCC 13+、Clang 17+、MSVC 19.29+ 才比较完整。老版本可以用 {fmt} 库,接口几乎一样。

std::format 返回新字符串。频繁调用会产生大量临时对象。性能敏感场景用 std::format_to 写入复用缓冲区。

std::print 是 C++23。只支持 C++20 时用 std::format 配合 cout。

{} 与 {{}}。要输出字面量花括号,需要写两个:

std::format("{{}}");   // "{}"

不要混用 printf 和 cout。如果程序里同时用了两者,输出顺序可能不符合预期(除非调用了 std::ios::sync_with_stdio(true),但那样会拖慢 cout)。

格式化宽字符。std::format 处理 char,宽字符用 std::format 的宽字符版本(std::wformat 系列),或者先转成 UTF-8。

5. 相关章节