跳到主要内容

C++ 20格式库

介绍

在C++20之前,当我们需要格式化输出时,通常会使用std::coutprintf或者std::stringstream等方式。这些方法各有优缺点:printf语法简洁但类型不安全,std::cout类型安全但语法冗长,而std::stringstream则更加灵活但使用起来较为复杂。

C++20引入了全新的格式库<format>,它结合了上述所有方法的优点,提供了一种现代化、类型安全且易于使用的文本格式化方式。这个库受到了Python中str.format()方法的启发,为C++程序员提供了更加简洁和强大的字符串格式化能力。

基本用法

引入头文件

要使用格式库,首先需要包含相应的头文件:

cpp
#include <format>

简单格式化

最基本的用法是使用std::format函数进行字符串格式化:

cpp
#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!
备注

在这个例子中,{}是占位符,会按照参数的顺序被替换。

带索引的格式化

你可以通过在花括号中指定索引来控制参数的使用顺序:

cpp
#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倍快乐!

高级格式化选项

格式说明符

格式库支持各种格式说明符,用于控制数值的显示方式:

cpp
#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大写十六进制
cpp
#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和输出操作,使代码更加简洁。

cpp
#include <print>  // C++23

int main() {
int answer = 42;
std::print("生命、宇宙以及一切的答案是:{}", answer);

return 0;
}

与传统方法的比较

让我们比较一下不同的字符串格式化方法:

cpp
#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格式库支持为自定义类型定义格式化规则:

cpp
#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)

实际应用案例

日志记录系统

格式库非常适合用于构建简单而强大的日志系统:

cpp
#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;
}

数据可视化

简单的字符图表生成器:

cpp
#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格式库为我们提供了一种现代化的字符串格式化解决方案,具有以下优势:

  1. 类型安全 - 与printf不同,格式错误会在编译时被捕获
  2. 语法简洁 - 比iostreamstringstream更清晰易读
  3. 灵活性强 - 支持多种格式选项和自定义类型格式化
  4. 性能更好 - 在许多情况下比传统方法更高效

随着C++23引入了std::print,格式化和输出变得更加简洁方便。学习和掌握格式库将大大提高您的C++代码质量和开发效率。

练习

  1. 修改上面的日志系统,添加彩色输出功能(控制台输出时使用ANSI转义序列)。
  2. 实现一个函数,使用格式库对表格数据进行漂亮的格式化输出。
  3. 为一个复数类(Complex)实现自定义格式器,支持直角坐标和极坐标两种输出格式。
  4. 使用格式库实现一个简单的进度条展示功能。

延伸阅读

掌握C++20格式库将使您的代码更加现代化和易于维护,特别是在处理复杂的文本格式化需求时。