C++ 20格式库
介绍
在C++20之前,当我们需要格式化输出时,通常会使用std::cout
、printf
或者std::stringstream
等方式。这些方法各有优缺点:printf
语法简洁但类型不安全,std::cout
类型安全但语法冗长,而std::stringstream
则更加灵活但使用起来较为复杂。
C++20引入了全新的格式库<format>
,它结合了上述所有方法的优点,提供了一种现代化、类型安全且易于使用的文本格式化方式。这个库受到了Python中str.format()
方法的启发,为C++程序员提供了更加简洁和强大的字符串格式化能力。
基本用法
引入头文件
要使用格式库,首先需要包含相应的头文件:
#include <format>
简单格式化
最基本的用法是使用std::format
函数进行字符串格式化:
#include <format>
#include <iostream>
int main() {
std::string name = "C++";
int version = 20;
std::string message = std::format("欢迎学习{}{}!", name, version);
std::cout << message << std::endl;
return 0;
}
输出结果:
欢迎学习C++20!
在这个例子中,{}
是占位符,会按照参数的顺序被替换。
带索引的格式化
你可以通过在花括号中指定索引来控制参数的使用顺序:
#include <format>
#include <iostream>
int main() {
std::string formatted = std::format("{1} 比 {0} 大", 5, 10);
std::cout << formatted << std::endl;
// 可以重复使用同一个参数
std::string repeated = std::format("{0},{0}!{1}倍快乐!", "你好", 2);
std::cout << repeated << std::endl;
return 0;
}
输出结果:
10 比 5 大
你好,你好!2倍快乐!
高级格式化选项
格式说明符
格式库支持各种格式说明符,用于控制数值的显示方式:
#include <format>
#include <iostream>
int main() {
// 设置小数点位数
double pi = 3.14159265358979;
std::cout << std::format("π值保留两位小数:{:.2f}", pi) << std::endl;
// 设置字段宽度和对齐方式
std::cout << std::format("靠左对齐:{:<10}|", "左边") << std::endl;
std::cout << std::format("居中对齐:{:^10}|", "中间") << std::endl;
std::cout << std::format("靠右对齐:{:>10}|", "右边") << std::endl;
// 使用填充字符
std::cout << std::format("使用星号填充:{:*^10}|", "星") << std::endl;
// 整数格式
int num = 42;
std::cout << std::format("十进制:{:d}", num) << std::endl;
std::cout << std::format("十六进制:0x{:x}", num) << std::endl;
std::cout << std::format("八进制:0{:o}", num) << std::endl;
std::cout << std::format("二进制:{:b}", num) << std::endl;
return 0;
}
输出结果:
π值保留两位小数:3.14
靠左对齐:左边 |
居中对齐: 中间 |
靠右对齐: 右边|
使用星号填充:***星***|
十进制:42
十六进制:0x2a
八进制:052
二进制:101010
整数表示方式
格式库支持多种整数表示方式:
格式 | 描述 |
---|---|
b | 二进制 |
d | 十进制(默认) |
o | 八进制 |
x | 小写十六进制 |
X | 大写十六进制 |
#include <format>
#include <iostream>
int main() {
int value = 255;
std::cout << std::format("十进制: {0:d}", value) << std::endl;
std::cout << std::format("十六进制(小写): {0:x}", value) << std::endl;
std::cout << std::format("十六进制(大写): {0:X}", value) << std::endl;
std::cout << std::format("带前缀的十六进制: {0:#x}", value) << std::endl;
std::cout << std::format("二进制: {0:b}", value) << std::endl;
std::cout << std::format("八进制: {0:o}", value) << std::endl;
return 0;
}
输出结果:
十进制: 255
十六进制(小写): ff
十六进制(大写): FF
带前缀的十六进制: 0xff
二进制: 11111111
八进制: 377
使用std::print(C++23)
虽然本文主要讨论C++20格式库,但值得一提的是,C++23引入了std::print
函数,它结合了std::format
和输出操作,使代码更加简洁。
#include <print> // C++23
int main() {
int answer = 42;
std::print("生命、宇宙以及一切的答案是:{}", answer);
return 0;
}
与传统方法的比较
让我们比较一下不同的字符串格式化方法:
#include <iostream>
#include <iomanip>
#include <sstream>
#include <format>
#include <cstdio>
int main() {
int age = 25;
double height = 1.75;
std::string name = "张三";
// 使用printf
printf("姓名: %s, 年龄: %d, 身高: %.2f米\n", name.c_str(), age, height);
// 使用cout
std::cout << "姓名: " << name << ", 年龄: " << age
<< ", 身高: " << std::fixed << std::setprecision(2) << height << "米" << std::endl;
// 使用stringstream
std::stringstream ss;
ss << "姓名: " << name << ", 年龄: " << age
<< ", 身高: " << std::fixed << std::setprecision(2) << height << "米";
std::string output = ss.str();
std::cout << output << std::endl;
// 使用format
std::string formatted = std::format("姓名: {}, 年龄: {}, 身高: {:.2f}米", name, age, height);
std::cout << formatted << std::endl;
return 0;
}
输出结果(所有方法都产生相同的结果):
姓名: 张三, 年龄: 25, 身高: 1.75米
姓名: 张三, 年龄: 25, 身高: 1.75米
姓名: 张三, 年龄: 25, 身高: 1.75米
姓名: 张三, 年龄: 25, 身高: 1.75米
从上面的比较可以看出,std::format
方法最为简洁和直观。
自定义格式化
C++20格式库支持为自定义类型定义格式化规则:
#include <format>
#include <iostream>
#include <string>
// 自定义结构体
struct Point {
int x;
int y;
};
// 为Point类型实现formatter特化
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) {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
int main() {
Point p{10, 20};
std::cout << std::format("点的坐标是: {}", p) << std::endl;
return 0;
}
输出结果:
点的坐标是: (10, 20)
实际应用案例
日志记录系统
格式库非常适合用于构建简单而强大的日志系统:
#include <format>
#include <iostream>
#include <chrono>
#include <string>
#include <fstream>
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
class Logger {
private:
std::ofstream logFile;
public:
Logger(const std::string& filename) {
logFile.open(filename, std::ios::app);
}
~Logger() {
if (logFile.is_open()) {
logFile.close();
}
}
template<typename... Args>
void log(LogLevel level, std::string_view formatStr, Args&&... args) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::string levelStr;
switch (level) {
case LogLevel::Debug: levelStr = "DEBUG"; break;
case LogLevel::Info: levelStr = "INFO"; break;
case LogLevel::Warning: levelStr = "WARNING"; break;
case LogLevel::Error: levelStr = "ERROR"; break;
}
std::string timeStr = std::ctime(&time);
timeStr.pop_back(); // 移除换行符
std::string message = std::format(formatStr, std::forward<Args>(args)...);
std::string logEntry = std::format("[{}] {} - {}", timeStr, levelStr, message);
if (logFile.is_open()) {
logFile << logEntry << std::endl;
}
std::cout << logEntry << std::endl;
}
};
int main() {
Logger logger("application.log");
logger.log(LogLevel::Info, "程序启动");
logger.log(LogLevel::Debug, "当前温度: {:.1f}°C", 23.5);
logger.log(LogLevel::Warning, "硬盘空间不足,剩余: {}GB", 10);
logger.log(LogLevel::Error, "文件 {} 无法打开", "config.ini");
return 0;
}
数据可视化
简单的字符图表生成器:
#include <format>
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
void printBarChart(const std::vector<double>& data, const std::vector<std::string>& labels) {
double maxValue = *std::max_element(data.begin(), data.end());
int maxBarLength = 50; // 最大条形长度
std::cout << "数据可视化条形图:" << std::endl;
std::cout << "==================" << std::endl;
for (size_t i = 0; i < data.size(); ++i) {
int barLength = static_cast<int>(data[i] / maxValue * maxBarLength);
std::string bar(barLength, '#');
std::cout << std::format("{:<10} |{:<50}| {:.1f}", labels[i], bar, data[i]) << std::endl;
}
}
int main() {
std::vector<double> sales = {254.5, 310.2, 290.8, 380.1, 201.5};
std::vector<std::string> months = {"一月", "二月", "三月", "四月", "五月"};
printBarChart(sales, months);
return 0;
}
输出可能类似于:
数据可视化条形图:
==================
一月 |################################### | 254.5
二月 |######################################### | 310.2
三月 |######################################## | 290.8
四月 |################################################## | 380.1
五月 |########################### | 201.5
总结
C++20格式库为我们提供了一种现代化的字符串格式化解决方案,具有以下优势:
- 类型安全 - 与
printf
不同,格式错误会在编译时被捕获 - 语法简洁 - 比
iostream
和stringstream
更清晰易读 - 灵活性强 - 支持多种格式选项和自定义类型格式化
- 性能更好 - 在许多情况下比传统方法更高效
随着C++23引入了std::print
,格式化和输出变得更加简洁方便。学习和掌握格式库将大大提高您的C++代码质量和开发效率。
练习
- 修改上面的日志系统,添加彩色输出功能(控制台输出时使用ANSI转义序列)。
- 实现一个函数,使用格式库对表格数据进行漂亮的格式化输出。
- 为一个复数类(Complex)实现自定义格式器,支持直角坐标和极坐标两种输出格式。
- 使用格式库实现一个简单的进度条展示功能。
延伸阅读
- C++标准: std::format
- P0645R10: Text Formatting - 格式库提案
- Victor Zverovich's Blog - 格式库作者的博客
掌握C++20格式库将使您的代码更加现代化和易于维护,特别是在处理复杂的文本格式化需求时。