目录

C++ 一出生就继承了 C 的 printf,但”格式化输出”的方案确一直在变,反正在我熟悉的编程语言里C++输出方案是最多的一门语言了。

C++98 搞出 std::cout 流式输出,C++20 搬来 Python 风格的 std::format(占位符 {}),C++23 干脆直接给了 std::print / std::println,格式化完直接落到 stdout。

嗯,没错一代补一代的坑,当然也可能一代更比一代坑。

本文汇总 C++ 各版本支持的格式化输出方案,每个方案附带完整可编译的 main 函数示例(所有输出均为真实程序运行结果),并逐项标注特性、优势、不足与使用注意事项。

温故而知新嘛,谁叫C++标准委员会这么能折腾呢!

格式化方案演进脉络

先上一张总表,对五个方案有个整体印象:

方案引入版本头文件核心思路类型安全编译期检查
C printf 家族C++98(继承自 C)格式说明符如: %d %f否否
std::cout 流式C++98<< 运算符重载是否
std::stringstreamC++98流式拼接成字符串是否
std::formatC++20{} 占位符 + 格式规范是是(consteval)
std::print / std::printlnC++23format 语法 + 直接输出是是

下面逐一展开。

方案一:C 风格 printf 家族

适用版本:C++98 / 03 / 11 / 14 / 17 / 20 / 23(全版本,继承自 C 标准)

// 编译: clang++ -std=c++98 c_printf_family.cpp -o c_printf_family
#include <cstdio>
#include <cstring>

int main() {
    // printf: 格式化输出到 stdout
    printf("printf: int=%d, float=%.2f, str=%s\n", 42, 3.14, "hello");

    // fprintf: 格式化输出到指定流 (stderr)
    fprintf(stderr, "fprintf: error code=%d, msg=%s\n", -1, "not found");

    // sprintf: 格式化写入缓冲区 (不安全, 不检查边界)
    char buf[64];
    sprintf(buf, "sprintf: id=%d, name=%s", 100, "Alice");
    printf("%s\n", buf);

    // snprintf: 格式化写入缓冲区 (安全, 指定最大长度)
    char sbuf[32];
    int written = snprintf(sbuf, sizeof(sbuf), "snprintf: pi=%.4f", 3.14159);
    printf("snprintf result: '%s', chars_needed=%d\n", sbuf, written);

    // 格式说明符速查:
    // %d 整数  %u 无符号  %ld 长整型  %f 浮点(printf 里实际收 double, 传入 float 会默认提升)
    // %lf 在 printf 中等价 %f(也是 double); %f 与 %lf 的区别只存在于 scanf: %f 对应 float*, %lf 对应 double*
    // %e 科学计数  %g 紧凑浮点  %c 字符  %s 字符串  %p 指针  %x/%X 十六进制
    // 宽度/精度: %5d  %-5d  %08d  %.2f  %10.2f  %*d (运行时宽度)
    int w = 8;
    printf("runtime width: [%*d]\n", w, 255);  // 右对齐宽度8

    return 0;
}

运行输出:

printf: int=42, float=3.14, str=hello
fprintf: error code=-1, msg=not found
sprintf: id=100, name=Alice
snprintf result: 'snprintf: pi=3.1416', chars_needed=19
runtime width: [     255]

特性

  • 通过格式说明符(%d、%s、%.2f 等)驱动格式化,语法紧凑。
  • 函数族完整:printf(stdout)、fprintf(指定流)、sprintf(缓冲区)、snprintf(安全缓冲区)、vprintf 系列(可变参数)。
  • 全版本可用,C/C++ 通用,无需任何 C++ 特性。

优势

  • 写起来最省字:一行搞定多类型混合输出。
  • 性能优秀:C 运行时直接操作缓冲区,无虚函数/动态分配开销。
  • 用的人最多:几乎所有文档、教程、调试输出都长 printf 这个样子。
  • 运行时宽度:%*d 支持运行时动态指定字段宽度,够灵活。

不足

  • 类型不安全:格式串与实参类型不匹配时(如 %d 传 double),行为未定义,不报编译错误。
  • 无编译期检查:编译器能做的只是 -Wformat 警告,且仅对字符串字面量有效。
  • sprintf 缓冲区溢出:不检查边界,是经典安全漏洞来源。C 标准从未弃用它,但所有现代编译器都强烈告警,实际工程里应一律改用 snprintf。
  • 不支持自定义类型:无法为自定义 class 直接输出,需手动转为基础类型。
  • 可读性下降:格式串越长,%d 与参数列表的对应关系越难追踪。

使用注意事项

  • 优先用 snprintf 替代 sprintf,永远不要在生产代码中使用 sprintf。
  • 检查 snprintf 返回值:若返回值 >= 缓冲区大小,说明被截断了。
  • 开启编译器警告:-Wformat -Wformat-security,避免格式串来自外部输入(格式串注入攻击)。
  • 避免用 %s 直接输出非 ‘\0’ 结尾的字符数组,用 %.Ns 限定最大长度。

方案二:std::cout 流式输出

适用版本:C++98 / 03 / 11 / 14 / 17 / 20 / 23

// 编译: clang++ -std=c++98 cout_stream.cpp -o cout_stream
#include <iostream>
#include <iomanip>

int main() {
    int    n = 42;
    double d = 3.14159;
    double e = 2.71828;

    // 基本输出: << 链式插入
    std::cout << "basic: " << "n=" << n << ", d=" << d << std::endl;

    // 精度控制: setprecision + fixed
    std::cout << std::fixed << std::setprecision(2);
    std::cout << "fixed(2): d=" << d << ", e=" << e << std::endl;

    // 宽度与填充: setw + setfill
    std::cout << std::setfill('0') << std::setw(8) << 255 << std::endl;
    std::cout << std::setw(10) << std::left << "Alice" << "|" << std::endl;

    // 十六进制输出: hex + showbase
    int hex_val = 255;
    std::cout << std::hex << std::showbase << hex_val << std::endl;
    std::cout << std::dec; // 恢复十进制

    // 布尔值文本: boolalpha
    bool flag = true;
    std::cout << std::boolalpha << "bool=" << flag << std::endl;
    std::cout << std::noboolalpha; // 恢复

    // 恢复默认格式
    std::cout.unsetf(std::ios::fixed);
    std::cout << std::setprecision(6);

    return 0;
}

运行输出:

basic: n=42, d=3.14159
fixed(2): d=3.14, e=2.72
00000255
Alice00000|
0xff
bool=true

特性

  • 通过 << 运算符重载实现类型安全的链式输出。
  • 格式控制依赖 I/O 操纵器(manipulator):std::setw、std::setprecision、std::hex、std::fixed 等。
  • 状态有粘性:std::hex、std::fixed 设置后持续生效,需手动恢复(std::dec、unsetf)。

优势

  • 类型安全:编译器根据实参类型选择正确的 operator<< 重载,类型不匹配直接编译错误。
  • 可扩展:为自定义类型重载 operator<< 即可直接输出,跟流式 API 直接配套。
  • 无需格式串:没有”格式说明符与参数对不上”的问题。
  • 流一致性:std::cout / std::cerr / std::ofstream / std::ostringstream 共享同一套接口。

不足

  • 语法冗长:std::cout << “x=” << x << ”, y=” << y 比 printf(“x=%d, y=%d”, x, y) 啰嗦得多。
  • 格式控制状态有粘性:std::hex 设置后忘记恢复,后续所有整数都变成十六进制,是经典 bug 来源。
  • 性能不如 printf:流式输出涉及多次虚函数调用(operator<<)和 locale 查询,比 printf 慢 2~5 倍。
  • 对齐/填充语法笨拙:std::setw(8) << std::setfill(‘0’) 不如 %08d 直观。

使用注意事项

  • std::endl 会刷新缓冲区,性能敏感场景用 ‘\n’ 替代。
  • 操纵器状态是粘性的,格式控制后记得恢复,或用 {} 作用域隔离。
  • std::setw 只对下一个输出项生效(无粘性),但 std::setfill 有粘性,而且对字符串同样生效:上文 cout 例子中 “Alice” 那行本想左对齐补空格,却因为前一行设的 setfill(‘0’) 没复位,右侧被补成了 00000 而不是空格。
  • 如果需要同步 C 标准 I/O,调用 std::ios::sync_with_stdio(false) 可提升性能,但会解除与 C stdio 的同步。

方案三:std::stringstream 字符串格式化

适用版本:C++98 / 03 / 11 / 14 / 17 / 20 / 23

// 编译: clang++ -std=c++98 stringstream.cpp -o stringstream
#include <sstream>
#include <iostream>
#include <iomanip>
#include <string>

int main() {
    // ostringstream: 将多种类型拼接成字符串
    std::ostringstream oss;
    int id = 1001;
    double score = 95.5;
    std::string name = "Alice";

    oss << "Student[id=" << id << ", name=" << name
        << ", score=" << score << "]";
    std::string result = oss.str();
    std::cout << "formatted: " << result << std::endl;

    // 精度控制
    std::ostringstream oss2;
    double pi = 3.14159265358979;
    oss2 << std::fixed << std::setprecision(4) << pi;
    std::cout << "pi(4dp): " << oss2.str() << std::endl;

    // istringstream: 从字符串解析 (替代 sscanf)
    std::string data = "42 3.14 hello";
    std::istringstream iss(data);
    int    val;
    float  fval;
    std::string word;
    iss >> val >> fval >> word;
    std::cout << "parsed: val=" << val << ", fval=" << fval
              << ", word=" << word << std::endl;

    // 实战场景: 构建日志/URL/SQL 片段
    std::ostringstream log;
    log << "[INFO] user_id=" << 9527 << " action=login status="
        << std::boolalpha << true;
    std::cout << log.str() << std::endl;

    return 0;
}

运行输出:

formatted: Student[id=1001, name=Alice, score=95.5]
pi(4dp): 3.1416
parsed: val=42, fval=3.14, word=hello
[INFO] user_id=9527 action=login status=true

特性

  • ostringstream 负责拼接(写),istringstream 负责解析(读),stringstream 双向皆可。
  • 格式控制复用 操纵器,与 std::cout 完全一致。
  • 产物是 std::string,可以继续传递、存储、输出。

优势

  • 类型安全:与 std::cout 同源,编译器检查类型。
  • 缓冲区自动增长:无需预估大小,不会溢出。
  • 双向能力:istringstream 可从字符串解析数据,替代 sscanf,类型安全。
  • 跟标准库配套顺:str() 返回 std::string,直接用于 STL 容器、文件 I/O 等。

不足

  • 性能开销大:每次构造 stringstream 都分配内部缓冲区,比 snprintf 慢一个数量级。
  • 语法同样冗长:链式 << 不比 std::cout 简洁。
  • 状态管理麻烦:复用同一个 stringstream 对象时需调用 .clear()(清错误标志)和 .str("")(清内容)。
  • 不适合高频调用:在热路径中反复构造/析构 stringstream 是常见性能反模式。

使用注意事项

  • 复用 stringstream 时:先 .str("") 清空内容,再 .clear() 清除 EOF/fail 标志。
  • 热路径中避免反复构造 stringstream,考虑预分配或改用 std::format / snprintf。
  • 解析以空白分隔,如需自定义分隔符需配合 std::getline。

  • C++20 之后,字符串拼接场景优先考虑 std::format,性能和可读性都更好。

方案四:std::format 格式化

适用版本:C++20 起,头文件

// 编译: clang++ -std=c++20 std_format.cpp -o std_format
#include <format>
#include <iostream>
#include <string>
#include <vector>

int main() {
    int    n = 42;
    double d = 3.14159;
    std::string name = "Alice";

    // 基本格式化: 占位符 {}
    std::string s1 = std::format("n={}, d={}, name={}", n, d, name);
    std::cout << s1 << std::endl;

    // 位置参数: {index}
    std::string s2 = std::format("{2} {1} {0}", "A", "B", "C");
    std::cout << s2 << std::endl;

    // 宽度与对齐: {:N} {:<N} {:>N} {:^N}
    std::cout << std::format("[{:>10}]\n", "right");
    std::cout << std::format("[{:<10}]\n", "left");
    std::cout << std::format("[{:^10}]\n", "center");

    // 填充字符: {:fill align width}
    std::cout << std::format("[{:*^10}]\n", "pad");

    // 精度: {:.Nf}
    std::cout << std::format("pi={:.4f}\n", d);
    std::cout << std::format("pi={:.10f}\n", d);

    // 整数格式: 二进制/八进制/十六进制
    int val = 255;
    std::cout << std::format("dec={} bin={:b} oct={:o} hex={:x}\n", val, val, val, val);
    std::cout << std::format("hex_upper={:X} with_prefix={:#x}\n", val, val);

    // 容器遍历格式化
    std::vector<int> nums = {1, 2, 3, 4, 5};
    for (size_t i = 0; i < nums.size(); ++i) {
        std::cout << std::format("nums[{}] = {}\n", i, nums[i]);
    }

    // format_to: 写入迭代器目标
    char buf[32];
    auto it = std::format_to(buf, "val={:0>5}", 42);
    *it = '\0';
    std::cout << "format_to: " << buf << std::endl;

    return 0;
}

运行输出:

n=42, d=3.14159, name=Alice
C B A
[     right]
[left      ]
[  center  ]
[***pad****]
pi=3.1416
pi=3.1415900000
dec=255 bin=11111111 oct=377 hex=ff
hex_upper=FF with_prefix=0xff
nums[0] = 1
nums[1] = 2
nums[2] = 3
nums[3] = 4
nums[4] = 5
format_to: val=00042

特性

  • 占位符语法 {},支持自动索引({} 按顺序)和位置索引({0} {1})。标准 C++20 不支持命名参数(那是 {fmt} 库的扩展),只能靠位置。
  • 格式规范语法:{:[fill][align][width][.precision][type]},一目了然。
  • 编译期格式串校验:格式串必须是常量表达式(consteval),语法错误在编译期报错。
  • 提供配套函数:std::format(返回 string)、std::format_to(写入迭代器)、std::format_to_n(限定写入量)、std::formatted_size(预计算长度)。
  • 支持自定义类型格式化:特化 std::formatter 模板即可。

优势

  • 类型安全 + 编译期检查:格式串语法错误编译期报错,类型不匹配编译期报错,双保险。
  • 性能优于 stringstream:内部预计算长度后一次性分配,无反复增长开销。
  • 语法简洁直观:std::format(“x={}, y={}”, x, y) 比 cout 链式和 printf 格式串都清晰。
  • Python 风格:写法跟 Python f-string / .format() 基本一个样子,上手不用重新学。
  • 可扩展:特化 std::formatter 让自定义类型直接支持格式化。
  • 安全:无缓冲区溢出风险,无格式串注入风险。

不足

  • 编译器支持要求高:需要完整 C++20 支持。GCC 13+(libstdc++ 完整实现)/ Clang 14+(libc++)/ MSVC 19.29+(VS2019 16.10+)才有,老旧工具链不可用。
  • 仍需配合 std::cout 输出:std::format 只返回字符串,不能直接输出到 stdout(C++23 才解决)。
  • 编译时间增加:模板元编程密集,大量使用会增加编译时间。
  • 自定义格式化器编写复杂:特化 std::formatter 涉及较多模板代码。

使用注意事项

  • 格式串必须是编译期常量,不能用运行时拼接的字符串做格式串(安全设计,防注入)。
  • 需要运行时格式串的场景,用 std::vformat + std::make_format_args(但失去编译期检查)。
  • std::format_to 写入裸数组时记得手动加 ‘\0’ 终止符。
  • 编译器需开启 -std=c++20 或更高,且 libc++ / libstdc++ 版本足够新。

方案五:std::print / std::println 直接格式化输出

适用版本:C++23 起,头文件

// 编译: clang++ -std=c++23 std_print.cpp -o std_print
#include <print>
#include <format>
#include <vector>

int main() {
    int    n = 42;
    double d = 3.14159;
    std::string name = "Alice";

    // std::print: 格式化直接输出到 stdout (不需要 std::cout)
    std::print("n={}, d={:.2f}, name={}\n", n, d, name);

    // std::println: 自动追加换行 (C++23)
    std::println("auto newline: val={:#x}", 255);
    std::println("aligned: [{:^15}]", "center");

    // 直接输出容器
    std::vector<int> nums = {10, 20, 30};
    for (size_t i = 0; i < nums.size(); ++i) {
        std::println("nums[{}] = {:0>4}", i, nums[i]);
    }

    // 输出到指定流 (C++23)
    FILE* f = stdout;
    std::print(f, "to_stream: {:>8}\n", 42);

    // 格式化但不输出: std::format 仍可用于获取字符串
    std::string s = std::format("stored: {:#b}", 255);
    std::println("{}", s);

    return 0;
}

运行输出:

n=42, d=3.14, name=Alice
auto newline: val=0xff
aligned: [    center     ]
nums[0] = 0010
nums[1] = 0020
nums[2] = 0030
to_stream:       42
stored: 0b11111111

特性

  • std::print:格式化并直接输出到 stdout 或指定 FILE*,无需中间字符串。
  • std::println:与 std::print 相同,但自动追加换行符。
  • 格式语法完全复用 std::format({} 占位符 + 格式规范)。
  • 编译期格式串校验,类型安全。
  • 内部实现可绕过 std::cout 的同步开销,直接操作 stdout 缓冲区。

优势

  • 一步到位:格式化 + 输出合二为一,不再需要 std::cout << std::format(…)。
  • 性能最优:直接写入 stdout,无中间 std::string 构造/拷贝/析构。
  • 类型安全 + 编译期检查:与 std::format 一脉相承。
  • std::println 极简:自动换行,省去手动 \n。
  • 可输出到任意 FILE*:std::print(stderr, …) 直接写错误流。

不足

  • 编译器支持最新:C++23 是最新标准,需要最新编译器(GCC 14+ / Clang 18+ / MSVC 19.34+(VS 2022)),大量存量项目无法使用。
  • 生态尚不成熟:第三方库、CI 环境、嵌入式工具链可能尚不支持。
  • 无 std::scan 对应物:C++23 尚未提供 std::scan(格式化输入),读取仍需其他方案。
  • 部分实现可能不完整:某些编译器的 实现仍在迭代中。

使用注意事项

  • 确认目标平台的编译器和标准库版本完整支持 C++23 。
  • std::println 会自动在末尾追加换行,默认追加 ‘\n’。若输出目标是 Windows 文本模式下的 FILE*,C 运行时会自动把 ‘\n’ 转成 ‘\r\n’,多数情况下不需要你手动区分平台。
  • std::print(f, …) 的 f 参数类型是 FILE*,不是 std::ostream&。
  • 如果项目尚不能用 C++23,推荐使用 {fmt} 库(std::format 的前身),API 几乎一致。

选型决策指南

按项目编译标准对号入座:

如果你的项目编译标准是 C++23

  • 直接用 std::print / std::println,格式化和输出一步完成,性能安全两头都占。
  • 需要获取字符串而非输出时,用 std::format。

如果你的项目编译标准是 C++20

  • 格式化成字符串用 std::format,输出配合 std::cout。
  • 简单调试输出仍可用 printf(但要开 -Wformat 警告)。

如果你的项目编译标准是 C++98/11/14/17

  • 性能敏感且类型简单:snprintf(安全版 C printf)。
  • 类型安全优先、需要拼接字符串:std::ostringstream。
  • 直接输出到 stdout:std::cout + 操纵器。
  • 如果能用第三方库:引入 {fmt},它是 std::format 的原型,API 几乎一致。

跨版本通用建议

  • 永远不要用 sprintf,用 snprintf。
  • std::endl 慎用,用 ‘\n’ 替代。
  • 格式串永远用字面量,不接受外部输入做格式串。

全方案对比速查表

维度printf 家族std::coutstringstreamstd::formatstd::print
引入版本C++98(C 继承)C++98C++98C++20C++23
类型安全否是是是是
编译期检查否否否是是
缓冲区安全snprintf 安全自动自动自动自动
性能高中低中高高
语法简洁度高低低高最高
自定义类型支持否是是是是
输出到 stdoutprintfcout需 cout需 cout直接
输出到字符串sprintf/snprintfostringstreamostringstreamformatformat
输入解析sscanfistreamistringstream无(C++23 无 scan)无
第三方等价物---{fmt} 库{fmt} 库

总结

  • C 风格 printf 家族全版本可用,性能最高但类型不安全,snprintf 是安全底线。
  • std::cout 和 std::stringstream 提供 C++ 原生的类型安全,但语法冗长、性能一般。
  • std::format(C++20)是个转折点:占位符是 Python 风格,编译期检查和类型安全都齐了,代价是要新编译器。
  • std::print / std::println(C++23)把这条路走到头了:格式化加输出一步到位,不用再借道 std::cout。
  • 选型核心原则:能用新版用新版(format/print),不能用新版用 snprintf + cout 组合,sprintf 能不用就不要用了。