格式化字符串输出几乎是每个C++项目都绕不开的需求。传统的printf速度快但类型不安全,传错参数轻则输出乱码,重则直接崩溃;iostreams虽然类型安全,可写起复合格式来又啰嗦又难读,比如控制浮点数精度和宽度时需要一堆setprecision、setw配合使用。fmt库正是为了解决这些问题而生,它是一个开源的现代化C++格式化库,语法上借鉴了Python的str.format,性能在多数场景下优于标准库方案,而且它还是C++20标准std::format的直接前身,学会fmt几乎等于提前掌握了std::format。本文将从安装配置讲起,逐步展开fmt的核心用法。

fmt库的安装与基本使用
fmt是一个header + source形式的库,获取方式主要有三种。第一种是直接使用包管理器,例如Ubuntu下执行sudo apt install libfmt-dev,macOS下用brew install fmt,vcpkg和Conan中也都提供了对应的包。第二种是从GitHub官方仓库下载源码,纳入CMake构建:
add_subdirectory(fmt) target_link_libraries(your_app PRIVATE fmt)
第三种是使用header-only模式,定义宏FMT_HEADER_ONLY后直接把头文件复制进项目即可,适合不想引入构建依赖的小项目。需要注意的是,从fmt 8.x开始推荐编译使用以获得更快的编译速度和更小的二进制体积,header-only只作为兜底方案。
装好之后最基础的用法就是fmt::format和fmt::print。前者返回格式化后的std::string,后者直接输出到stdout并自带换行控制。看一个例子:
#include <fmt/core.h>
int main() {
std::string s = fmt::format("你好, {}! 你今年 {} 岁了", "小明", 18);
fmt::print("{}\n", s);
fmt::print("圆周率约为 {:.3f}\n", 3.14159265);
return 0;
}输出结果分别是拼接好的字符串和保留三位小数的3.142。可以看到,占位符{}按顺序接收参数,不需要像printf那样区分%d、%s、%f,传参类型由编译器自动推导,从根本上杜绝了格式串与实参类型不匹配的隐患。这一点是fmt相对printf最核心的安全优势:它不存在未定义行为的温床。
格式说明符详解:位置索引与格式控制
fmt的占位符远不止简单的{},完整语法是{[索引][:格式说明]}。通过索引可以复用参数或调整输出顺序,这在做多语言国际化时特别有用,因为不同语言的语序往往不同:
fmt::print("{0} 加 {1} 等于 {2}, {1} 加 {0} 也等于 {2}\n", 1, 2, 3);格式说明符部分的结构为[[fill]align][sign][#][0][width][.precision][type],每个字段都可以省略。常用的几个示例如下:
fmt::print("{:>10}|\n", "右对齐"); // 宽度10,右对齐
fmt::print("{:*^10}|\n", "居中"); // 用*填充,居中
fmt::print("{:+.2f}\n", 3.14159); // 强制显示正号,保留2位小数
fmt::print("{:08.3f}\n", 3.14159); // 宽度8,前导补零
fmt::print("{:#x}\n", 255); // 十六进制并带0x前缀,输出0xff
fmt::print("{:,}\n", 1234567); // 千位分隔符,输出1,234,567对齐字符中<表示左对齐、>右对齐、^居中,填充字符写在align前面。整数的type部分支持b(二进制)、o(八进制)、x/X(十六进制)、d(十进制),浮点数支持f/e/g等,含义与printf基本一致但写法更统一。相比iostreams需要iomanip那一堆操纵符,fmt把所有格式控制集中在一个说明符里,可读性和维护性都好得多。
fmt还支持命名参数,通过fmt::arg或者参数列表语法让代码自解释:
fmt::print("用户 {name} 的余额为 {balance:.2f} 元\n",
fmt::arg("name", "张三"), fmt::arg("balance", 998.5));命名参数在格式串被外部化(比如存到配置文件或翻译文件)时价值最大,阅读格式串就能知道每个位置填的是什么语义的内容,而不是靠数参数顺序去猜。
格式化自定义类型与安全编译期检查
fmt真正强大的地方在于可扩展性。任何自定义类型只要提供formatter特化,就能直接用{}输出。以一个简单的Point结构体为例:
#include <fmt/format.h>
struct Point {
double x, y;
};
template <> struct fmt::formatter<Point> : fmt::formatter<double> {
auto format(const Point& p, fmt::format_context& ctx) const {
return fmt::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
int main() {
fmt::print("{}", Point{1.5, 2.5}); // 输出 (1.5, 2.5)
}另一种更简单的途径是重载operator<<让它支持ostream,然后包含<fmt/ostream.h>,fmt会自动复用已有的输出逻辑。两种方式各有取舍:formatter特化性能更好且不依赖iostream,operator<<复用则省去重复代码,团队里已有大量ostream输出函数时推荐后者。
安全性方面,fmt默认开启了编译期格式串检查。如果格式说明符和参数类型不匹配,例如对整数使用{:.2f},编译器会直接报错,把问题拦截在构建阶段。如果格式串来自运行时的变量(比如从配置读入),可以改用fmt::runtime包装,此时退回运行时检查,用法是fmt::format(fmt::runtime(user_str), args...)。这种显式区分编译期与运行时格式的机制,比printf的全运行时行为和iostream的无格式约束都更严谨。
性能层面,fmt做了大量优化,包括编译期计算格式串长度、返回SBO(小字符串优化)的string、以及零开销的参数打包。官方benchmark显示其format_to_n等接口在多数编译器上比snprintf快,比ostream快数倍。对于高频日志输出场景,性能差距相当可观,这也是spdlog等主流日志库底层选择fmt的原因。
fmt与C++20 std::format的关系及迁移建议
fmt的作者Victor Zverovich主持了C++20 std::format的提案,因此std::format在API设计上与fmt高度一致,占位符语法、格式说明符几乎完全相同。一个基本的对照:
// fmt 版本
#include <fmt/core.h>
fmt::print("{} {}\n", 42, "hello");
// C++20 std 版本,需要 gcc 13+ / clang 17+ / msvc 19.29+ 的较完整实现
#include <format>
#include <iostream>
std::cout << std::format("{} {}\n", 42, "hello");两者的主要差异在于:std::format由标准库提供,无需第三方依赖;但fmt的功能更新更及时,例如编译期格式检查在std::format中落地较晚,且fmt还提供了fmt/ranges.h这样的扩展,可以直接格式化整个容器:
#include <fmt/ranges.h>
#include <vector>
std::vector<int> v{1, 2, 3, 4};
fmt::print("{}\n", v); // 输出 [1, 2, 3, 4]容器格式化在调试打印、日志记录时非常实用,省去了手写循环拼接的麻烦。目前std::format对容器输出尚无标准支持,这也是很多团队即使编译器支持C++20仍继续用fmt的原因。
实际项目中的建议是:如果项目已经能使用较新的工具链且格式化需求简单,直接用std::format减少依赖;如果需要兼容老编译器、依赖ranges格式化、或者追求极致性能与更完整的编译期检查,fmt仍是首选。两者API几乎通用,从fmt迁移到std::format基本只需要把fmt::前缀换成std::并替换头文件,迁移成本极低。无论选择哪条路,都建议新代码彻底告别printf和手工字符串拼接,让格式化这件事变得既安全又优雅。
C++ fmt库fmt format用法字符串格式化修改时间:2026-09-14 05:24:46