在C++17中引入的std::filesystem库为文件系统操作提供了统一且现代化的接口,极大地简化了文件和目录的处理。本文通过一个完整的实战案例,展示如何利用std::filesystem完成日志文件的滚动、文件夹扫描以及跨平台路径处理,并在代码中加入异常安全与性能优化的细节。
1. 需求分析
假设我们正在开发一款需要持续写入日志的服务器程序。每写入一个日志文件超过100MB,系统会自动切分成新的文件,保留最近N个日志文件,过旧的文件则被删除。与此同时,程序还需要能扫描日志目录,生成一个包含所有日志文件大小的报告,支持Windows、Linux和macOS三大主流平台。
2. 关键技术点
| 技术 | 说明 | 关键代码 |
|---|---|---|
| 路径操作 | 使用std::filesystem::path自动处理不同系统下的路径分隔符。 |
auto p = std::filesystem::path{log_dir} / "log.txt"; |
| 文件大小检测 | std::filesystem::file_size获取文件字节数。 |
auto sz = std::filesystem::file_size(p); |
| 目录遍历 | std::filesystem::recursive_directory_iterator递归遍历目录。 |
for (auto const & entry : std::filesystem::recursive_directory_iterator(dir)) |
| 异常安全 | 所有filesystem函数抛异常时使用try-catch捕获,并记录错误。 | try { ... } catch(const std::filesystem::filesystem_error& e) {} |
| 性能优化 | 采用std::filesystem::directory_iterator而不是递归迭代器,除非需要递归;使用std::filesystem::space获取磁盘剩余空间。 |
auto space = std::filesystem::space(dir); |
3. 代码实现
下面的代码片段展示了完整的日志滚动与报告生成逻辑。为了便于阅读,已删除了不必要的头文件与宏定义,直接列出核心实现。
#include <filesystem>
#include <fstream>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <sstream>
namespace fs = std::filesystem;
// 生成唯一的日志文件名,例如 log_20240113_142500.txt
std::string generate_log_name(const fs::path& dir)
{
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::tm tm;
#if defined(_WIN32) || defined(_WIN64)
localtime_s(&tm, &time);
#else
localtime_r(&time, &tm);
#endif
std::ostringstream oss;
oss << "log_" << std::put_time(&tm, "%Y%m%d_%H%M%S") << ".txt";
return (dir / oss.str()).string();
}
// 关闭当前日志文件并创建新文件
void rollover_log(std::ofstream& ofs, const fs::path& dir, std::string& current_name)
{
ofs.close();
current_name = generate_log_name(dir);
ofs.open(current_name, std::ios::out | std::ios::app);
}
// 只保留最近N个日志文件
void prune_old_logs(const fs::path& dir, std::size_t keep = 5)
{
std::vector<fs::directory_entry> logs;
for (auto const& entry : fs::directory_iterator(dir))
{
if (entry.is_regular_file() && entry.path().extension() == ".txt")
logs.push_back(entry);
}
// 按修改时间排序,旧的在前
std::sort(logs.begin(), logs.end(),
[](auto const& a, auto const& b) {
return fs::last_write_time(a) < fs::last_write_time(b);
});
if (logs.size() <= keep) return;
for (std::size_t i = 0; i < logs.size() - keep; ++i)
{
try { fs::remove(logs[i].path()); }
catch (const fs::filesystem_error& e) { std::cerr << e.what() << '\n'; }
}
}
// 生成日志目录报告
void generate_report(const fs::path& dir, const fs::path& report_file)
{
std::ofstream rpt(report_file, std::ios::out);
rpt << "Log Report - " << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) << '\n';
rpt << "---------------------------------------------------\n";
std::uintmax_t total_size = 0;
for (auto const& entry : fs::directory_iterator(dir))
{
if (!entry.is_regular_file() || entry.path().extension() != ".txt") continue;
auto sz = fs::file_size(entry.path());
rpt << std::setw(40) << std::left << entry.path().filename().string() << std::right << std::setw(12) << sz << " bytes\n";
total_size += sz;
}
rpt << "---------------------------------------------------\n";
rpt << std::setw(40) << "Total Size:" << std::right << std::setw(12) << total_size << " bytes\n";
}
// 主程序入口示例
int main()
{
const fs::path log_dir = "./logs";
try
{
if (!fs::exists(log_dir)) fs::create_directories(log_dir);
}
catch (const fs::filesystem_error& e)
{
std::cerr << "创建日志目录失败: " << e.what() << '\n';
return 1;
}
std::string current_log = generate_log_name(log_dir);
std::ofstream ofs(current_log, std::ios::out | std::ios::app);
if (!ofs.is_open())
{
std::cerr << "打开日志文件失败\n";
return 1;
}
// 简化示例:每秒写入一行,超过100MB时滚动
const std::uintmax_t MAX_SIZE = 100 * 1024 * 1024; // 100 MB
for (int i = 0; ; ++i)
{
ofs << "Log entry #" << i << " at " << std::chrono::system_clock::now().time_since_epoch().count() << '\n';
if (ofs.tellp() >= static_cast<std::streamoff>(MAX_SIZE))
rollover_log(ofs, log_dir, current_log);
std::this_thread::sleep_for(std::chrono::seconds(1));
// 每5分钟执行一次日志清理和报告生成
if (i % 300 == 0)
{
prune_old_logs(log_dir, 5);
generate_report(log_dir, log_dir / "report.txt");
}
}
return 0;
}
代码说明
-
路径统一
fs::path把 Windows 的\\与 POSIX 的/自动转义,保证跨平台兼容。 -
异常处理
所有文件系统操作都被包裹在try-catch里,防止因磁盘错误导致程序崩溃。 -
性能注意
- 使用
fs::directory_iterator而非递归迭代器,因为日志目录一般不需要递归。 - 只在必要时读取文件大小,避免无谓的 I/O。
- 使用
-
可扩展性
- 可以通过配置文件调整
MAX_SIZE、保留文件数量等参数。 - 报告文件支持输出为 CSV 或 JSON,方便后续分析。
- 可以通过配置文件调整
4. 总结
std::filesystem用起来几乎无需额外库,标准化的接口为文件系统操作带来了极大的便利。- 正确的路径拼接、异常安全与性能优化是编写健壮跨平台代码的三大核心。
- 在C++17及之后的标准中,
std::filesystem已成为处理文件的首选工具,任何涉及文件读写的项目都值得优先考虑使用。
通过上述案例,读者可以快速上手 std::filesystem,并将其应用于日志管理、配置文件解析、资源预加载等多种实际场景。祝编码愉快!