还在用 fopen / opendir / stat 这些 C 风格 API 处理文件?C++17 引入的 std::filesystem 可能是你见过的最优雅的文件操作方案——一套 API 打天下,跨平台、面向对象、功能齐全。
前言
在 C++17 之前,处理文件路径和目录是件痛苦的事:
- Linux 下 用
opendir / readdir / closedir 遍历目录 - Windows 下 用
FindFirstFile / FindNextFile / FindClose - 跨平台? 要么手写条件编译,要么引入 Boost
结果:代码里充斥着平台相关的 ifdef,维护噩梦。
C++17 统一了文件操作——std::filesystem 来了。
一、fs::path:路径操作的瑞士军刀
fs::path 是文件系统的核心,它把「路径字符串」抽象成对象,提供大量好用的成员函数。
1.1 路径解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> #include <filesystem> namespace fs = std::filesystem;
int main() { fs::path p = "/home/user/documents/file.txt"; std::cout << "完整路径: " << p << "\n"; std::cout << "文件名: " << p.filename() << "\n"; std::cout << "不含扩展名: " << p.stem() << "\n"; std::cout << "扩展名: " << p.extension() << "\n"; std::cout << "父目录: " << p.parent_path() << "\n"; std::cout << "根目录: " << p.root_name() << "\n"; std::cout << "根路径: " << p.root_path() << "\n"; }
|
1.2 路径拼接
/ 操作符被重载,拼接路径再也不用手动加 /:
1 2 3
| fs::path base = "/home"; fs::path full = base / "user" / "documents" / "file.txt"; std::cout << full << "\n";
|
注意:C++17 的 fs::path 会自动处理多余的 /,你不用关心 base 后面有没有斜杠。
1.3 路径遍历
fs::path 可以像容器一样遍历每个路径元素:
1 2 3 4 5
| fs::path p = "/home/user/documents"; for (const auto& part : p) { std::cout << part << "\n"; }
|
二、目录遍历:directory_iterator
2.1 基本遍历
1 2 3 4
| for (const auto& entry : fs::directory_iterator(".")) { std::cout << entry.path().filename() << "\n"; }
|
2.2 递归遍历
recursive_directory_iterator 会递归进入子目录:
1 2 3 4
| for (const auto& entry : fs::recursive_directory_iterator("/tmp")) { std::cout << entry.path() << "\n"; }
|
2.3 过滤遍历
可以结合 directory_options 跳过权限错误:
1 2 3 4 5
| for (const auto& entry : fs::recursive_directory_iterator(".", fs::directory_options::skip_permission_denied)) { }
|
三、文件状态判断
3.1 常见判断函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| fs::path p = "test.txt";
if (fs::exists(p)) { std::cout << "文件大小: " << fs::file_size(p) << " bytes\n"; }
if (fs::is_regular_file(p)) { std::cout << "是普通文件\n"; }
if (fs::is_directory(p)) { std::cout << "是目录\n"; }
if (fs::is_symlink(p)) { std::cout << "是符号链接\n"; }
|
3.2 用 status() 一次性获取
1 2 3 4 5
| fs::path p = "test.txt"; fs::file_status s = fs::status(p); std::cout << std::boolalpha; std::cout << "存在: " << fs::exists(s) << "\n"; std::cout << "是文件: " << fs::is_regular_file(s) << "\n";
|
四、目录与文件操作
4.1 创建目录
1 2 3 4 5
| fs::create_directory("/tmp/mydir");
fs::create_directories("/tmp/a/b/c/subdir");
|
4.2 删除操作
1 2 3 4 5 6 7 8
| fs::remove("temp.txt");
fs::remove("/tmp/emptydir");
fs::remove_all("/tmp/cleanup");
|
4.3 复制 / 移动
1 2 3 4 5 6 7 8 9
| fs::copy_file("a.txt", "b.txt", fs::copy_options::overwrite_existing);
fs::copy_directory("src", "dst");
fs::rename("old.txt", "new.txt"); fs::rename("file.txt", "/new/location/file.txt");
|
五、工程应用示例
5.1 配置加载框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream> #include <filesystem> #include <fstream> #include <string>
namespace fs = std::filesystem;
class ConfigLoader { public: bool loadFromDir(const fs::path& dir) { if (!fs::exists(dir) || !fs::is_directory(dir)) { std::cerr << "目录不存在: " << dir << "\n"; return false; } for (const auto& entry : fs::directory_iterator(dir)) { if (entry.is_regular_file() && entry.path().extension() == ".conf") { loadFile(entry.path()); } } return true; } private: void loadFile(const fs::path& p) { std::ifstream fin(p); std::cout << "加载配置: " << p.filename() << "\n"; } };
|
5.2 文件监控框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <filesystem> #include <chrono> #include <thread>
namespace fs = std::filesystem;
class FileWatcher { public: void watch(const fs::path& dir, int interval_sec = 2) { std::cout << "监控目录: " << dir << "\n"; while (true) { try { for (const auto& entry : fs::recursive_directory_iterator(dir)) { auto mtime = fs::last_write_time(entry); if (mtime > last_check_) { std::cout << "检测到变化: " << entry.path() << "\n"; } } } catch (const fs::filesystem_error& e) { std::cerr << "监控错误: " << e.what() << "\n"; } last_check_ = fs::file_time_type::clock::now(); std::this_thread::sleep_for(std::chrono::seconds(interval_sec)); } } private: fs::file_time_type last_check_ = fs::file_time_type::clock::now(); };
|
六、boost::filesystem vs std::filesystem
| 维度 | boost::filesystem | std::filesystem |
|---|
| 版本 | Boost 1.46+ (2008) | C++17 (2017) |
| 头文件 | <boost/filesystem.hpp> | <filesystem> |
| 命名空间 | boost::filesystem | std::filesystem |
| 跨平台 | ✅ 需编译 Boost | ✅ 标准支持,编译器自带 |
| ABI 兼容性 | Boost 版本相关 | 标准保证 |
| 推荐 | 旧代码维护 | 新项目首选 |
结论:新项目直接用 std::filesystem,没有理由再用 Boost 的版本。
七、完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <filesystem> namespace fs = std::filesystem;
int main() { fs::path p = "/home/user/documents/file.txt"; std::cout << "filename: " << p.filename() << "\n"; std::cout << "stem: " << p.stem() << "\n"; std::cout << "extension: " << p.extension() << "\n"; std::cout << "parent: " << p.parent_path() << "\n"; for (const auto& entry : fs::directory_iterator(".")) { std::cout << entry.path().filename() << "\n"; } fs::path base = "/home"; fs::path full = base / "user" / "file.txt"; std::cout << full << "\n"; if (fs::exists("test.txt")) { std::cout << "size: " << fs::file_size("test.txt") << "\n"; } fs::create_directories("/tmp/test/subdir"); fs::copy_file("a.txt", "b.txt", fs::copy_options::overwrite_existing); }
|
行动建议:把项目里散落在各处的 FILE* / opendir / GetFileAttributes 统一替换成 std::filesystem 吧——代码会更短、更安全、更好维护。
📚 C++17 新特性 系列导航
本文是《C++17 新特性》系列第 7/8 篇。
📖 全部 8 篇目录(点击展开)
- (一)Structured Bindings
- (二)if constexpr
- (三)Inline Variables 与 constexpr 加强
- (四)Fold Expressions
- (五)std::optional / variant / any
- (六)std::apply / std::invoke
- (七)Filesystem 大全 ← 当前
- (八)Attribute 新增
语法/控制流可视化
下面是一张马卡龙色 Mermaid 图,帮你从图形角度把握本章涉及的语法与控制流。
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FFE5EC', 'primaryTextColor': '#5D5D5D', 'primaryBorderColor': '#FFB3C6', 'lineColor': '#B5EAD7', 'secondaryColor': '#C7CEEA', 'tertiaryColor': '#FFDAC1'}}}%%
flowchart TD
P["📂 std::filesystem::path"] --> OP{"🛠️ 执行操作"}
OP -- "exists / is_directory" --> Q["🔎 查询状态"]
OP -- "create_directory / remove" --> R["✏️ 修改文件系统"]
OP -- "directory_iterator" --> I["🔁 惰性遍历"]
OP -- "file_size / last_write_time" --> M["📊 元数据"]
Q --> ERR{"❓ 出错?"}
R --> ERR
I --> ERR
M --> ERR
ERR -- "是" --> E["💥 抛出<br/>filesystem_error"]
ERR -- "否" --> OK["✅ 返回结果"]
E --> H["🧯 try/catch<br/>或 error_code 重载"]
OK --> DONE["🎯 继续业务流程"]
H --> DONE
style P fill:#FFE5EC,stroke:#FFB3C6,color:#5D5D5D
style OP fill:#FFDAC1,stroke:#FFB3C6,color:#5D5D5D
style Q fill:#C7CEEA,stroke:#A8DADC,color:#5D5D5D
style R fill:#B5EAD7,stroke:#A8DADC,color:#5D5D5D
style I fill:#C7CEEA,stroke:#A8DADC,color:#5D5D5D
style M fill:#FFDAC1,stroke:#FFB3C6,color:#5D5D5D
style ERR fill:#FFE5EC,stroke:#FF9AA2,color:#5D5D5D
style E fill:#FFDAC1,stroke:#FF9AA2,color:#5D5D5D
style H fill:#C7CEEA,stroke:#A8DADC,color:#5D5D5D
style OK fill:#B5EAD7,stroke:#A8DADC,color:#5D5D5D
style DONE fill:#FFE5EC,stroke:#FFB3C6,color:#5D5D5D
本章讲 C++17 的 std::filesystem,对比维度:本特性 vs C / POSIX 旧写法 vs 其他语言文件 API。
对比分析
一、本特性 vs 旧写法
| 维度 | C++17 写法 | C / POSIX 写法 | 影响 |
|---|
| 路径 | std::filesystem::path p("a/b.txt"); | char* / std::string | 类型化、自动归一化 |
| 检查存在 | fs::exists(p) | access(p.c_str(), F_OK) | 可读性 |
| 创建目录 | fs::create_directories(p) | mkdir(...)(需多级递归) | 一行 |
| 列目录 | for (auto& e : fs::directory_iterator(p)) | opendir/readdir/closedir | RAII + 异常 |
| 文件大小 | fs::file_size(p) | stat(...) | 跨平台 |
二、对比其他语言
| 语言 | 文件系统库 | 风格 |
|---|
| C++17 | std::filesystem | 面向对象 + RAII |
| Python | pathlib / os | OO / 函数式混搭 |
| Java | java.nio.file.Files | 静态方法 + Path |
| Rust | std::fs | Result + 强类型 |
| Go | os / filepath | 函数式 + error |
三、优缺点
优点:
- 跨平台统一 API
- 与 STL 风格一致:迭代器、异常、类型化
缺点:
- 异常 / 错误码两种风格混用,调用前需选好策略
- 在某些嵌入式平台支持有限
四、何时选
- 新项目:直接使用 std::filesystem
- 维护老代码:可逐步替换关键路径