目录
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::stringstream | C++98 | 流式拼接成字符串 | 是 | 否 | |
| std::format | C++20 | {} 占位符 + 格式规范 | 是 | 是(consteval) | |
| std::print / std::println | C++23 | format 语法 + 直接输出 | 是 | 是 |
下面逐一展开。
方案一: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::cout | stringstream | std::format | std::print |
|---|---|---|---|---|---|
| 引入版本 | C++98(C 继承) | C++98 | C++98 | C++20 | C++23 |
| 类型安全 | 否 | 是 | 是 | 是 | 是 |
| 编译期检查 | 否 | 否 | 否 | 是 | 是 |
| 缓冲区安全 | snprintf 安全 | 自动 | 自动 | 自动 | 自动 |
| 性能 | 高 | 中 | 低 | 中高 | 高 |
| 语法简洁度 | 高 | 低 | 低 | 高 | 最高 |
| 自定义类型支持 | 否 | 是 | 是 | 是 | 是 |
| 输出到 stdout | printf | cout | 需 cout | 需 cout | 直接 |
| 输出到字符串 | sprintf/snprintf | ostringstream | ostringstream | format | format |
| 输入解析 | sscanf | istream | istringstream | 无(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 能不用就不要用了。