在 C++17 中, 头文件正式加入标准库,为文件和目录的操作提供了统一且跨平台的接口。相比之前常用的 boost::filesystem 或系统特定的 API,std::filesystem 的优点在于标准化、简洁以及与 STL 的紧密结合。本文将通过一个完整的小项目来演示如何使用 std::filesystem 实现一个简易的“文件监视器”,并讨论其中的一些常见坑和优化技巧。
1. 环境准备
- 编译器:g++ 9+ / clang++ 9+ / MSVC 2019+
- C++17 标准开启:
-std=c++17 - 需要链接
stdc++fs(在某些旧版 GCC 上):-lstdc++fs
2. 需求概述
我们要实现一个命令行工具,监视指定目录下的文件变动(新增、删除、修改),并将变动记录到日志文件中。主要功能点:
- 目录遍历:递归读取子目录。
- 文件状态存储:使用
std::unordered_map<std::filesystem::path, std::filesystem::file_time_type>存放文件路径及其最后修改时间。 - 变动检测:每隔一段时间(如 1 秒)重新遍历并比对上一次的状态。
- 日志输出:记录变动事件,格式为
[时间戳] 事件类型: 路径.
3. 核心代码实现
#include <filesystem>
#include <unordered_map>
#include <chrono>
#include <thread>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <ctime>
namespace fs = std::filesystem;
// 用于将 file_time_type 转成可读时间字符串
std::string time_to_string(fs::file_time_type ft) {
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ft - fs::file_time_type::clock::now()
+ std::chrono::system_clock::now());
std::time_t t = std::chrono::system_clock::to_time_t(sctp);
std::tm tm;
#ifdef _WIN32
localtime_s(&tm, &t);
#else
localtime_r(&t, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
// 记录日志
void log_event(const std::string& evt, const fs::path& p) {
static std::ofstream ofs("file_monitor.log", std::ios::app);
if (!ofs.is_open()) {
std::cerr << "Cannot open log file!\n";
return;
}
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm;
#ifdef _WIN32
localtime_s(&tm, &t);
#else
localtime_r(&t, &tm);
#endif
std::ostringstream oss;
oss << "[" << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << "] " << evt << ": " << p << "\n";
ofs << oss.str();
ofs.flush();
}
// 递归遍历目录,更新状态映射
void scan_directory(const fs::path& root,
std::unordered_map<fs::path, fs::file_time_type>& state) {
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (entry.is_regular_file()) {
state[entry.path()] = entry.last_write_time();
}
}
}
// 检测差异并记录
void detect_and_log(const std::unordered_map<fs::path, fs::file_time_type>& prev,
const std::unordered_map<fs::path, fs::file_time_type>& curr) {
// 处理新增或修改
for (const auto& [p, t] : curr) {
auto it = prev.find(p);
if (it == prev.end()) {
log_event("新增", p);
} else if (it->second != t) {
log_event("修改", p);
}
}
// 处理删除
for (const auto& [p, _] : prev) {
if (curr.find(p) == curr.end()) {
log_event("删除", p);
}
}
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: file_monitor <directory>\n";
return 1;
}
fs::path dir(argv[1]);
if (!fs::exists(dir) || !fs::is_directory(dir)) {
std::cerr << "Invalid directory!\n";
return 1;
}
std::unordered_map<fs::path, fs::file_time_type> prev;
scan_directory(dir, prev);
std::cout << "开始监视目录: " << dir << "\n";
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::unordered_map<fs::path, fs::file_time_type> curr;
scan_directory(dir, curr);
detect_and_log(prev, curr);
prev.swap(curr);
}
return 0;
}
4. 常见坑与优化
| 场景 | 说明 | 解决办法 |
|---|---|---|
| 跨平台文件时间精度 | Windows last_write_time 的精度为 100 ns,但在旧 Windows 版本中可能只有 1 ms |
对时间进行比较时可以放宽误差阈值,或使用 std::chrono::duration_cast<std::chrono::milliseconds> |
| 符号链接 | recursive_directory_iterator 默认会遍历符号链接,可能导致无限递归 |
通过 fs::directory_options::follow_directory_symlink 或自行过滤 is_symlink() |
| 文件删除后仍在迭代器中 | 删除后 last_write_time() 会抛异常 |
在遍历前捕获异常或检查 exists() |
| 日志文件过大 | 持续写日志可能导致磁盘空间耗尽 | 添加日志轮转机制,或使用日志库如 spdlog |
5. 进一步扩展
- 事件过滤:只监视特定后缀的文件(
.cpp,.h等)。 - 多线程:将扫描和日志写分离,使用
std::queue+ 生产者/消费者模型。 - 网络同步:将变动信息推送到远程服务器或通过 WebSocket 进行实时推送。
- 事件回调:提供回调接口,让用户自行处理事件。
6. 总结
std::filesystem 的引入大幅简化了文件系统操作的代码量和可读性。通过以上示例,我们可以看到它在目录递归、文件属性查询、错误处理等方面都比传统的 Boost/OS 特定 API 更加直观。希望这篇实战文章能帮助你快速上手,并在自己的项目中合理利用 std::filesystem 的强大功能。