4. 文件系统抽象:std::filesystem的跨平台使用、路径处理、目录遍历
文件系统操作,说白了就是跟文件夹和文件打交道。你在Windows上写路径用反斜杠,到了Linux上就得用正斜杠,光这个就够让人头疼的。C++17引入的std::filesystem,就是为了解决这类跨平台问题。
我个人习惯,只要项目允许用C++17,文件操作一律用std::filesystem。为什么?因为它把平台差异都封装好了,你写一遍代码,Windows、Linux、macOS都能跑。省心。
4.1 路径处理:别再手动拼接字符串了
很多新手喜欢这么写路径:
std::string path = "C:\\Users\\me\\data\\" + filename;
嗯,这里有个坑。换到Linux上,这个路径就废了。而且你还要处理末尾有没有斜杠、路径里有没有非法字符……烦不烦?
用std::filesystem::path就清爽多了:
#include <filesystem>
namespace fs = std::filesystem;
fs::path base = "C:/Users/me/data";
fs::path full = base / "report.txt"; // 自动处理分隔符
std::cout << full.string(); // 输出: C:/Users/me/data/report.txt
注意那个/运算符。它会自动根据当前平台插入正确的分隔符。你在Windows上跑,它就用反斜杠;在Linux上跑,它就用正斜杠。我在项目中遇到过有人手动加\\,结果代码移植到服务器上直接崩溃——路径全错了。
path::preferred_separator可以获取当前平台的分隔符字符。调试时打印出来看看,心里有数。
4.2 路径的常用操作
path对象提供了很多实用的成员函数。我列几个最常用的:
| 函数 | 作用 | 示例 |
|---|---|---|
filename() |
获取文件名(含扩展名) | "report.txt" |
stem() |
获取文件名(不含扩展名) | "report" |
extension() |
获取扩展名 | ".txt" |
parent_path() |
获取父目录 | "C:/Users/me/data" |
root_path() |
获取根路径 | "C:/" |
relative_path() |
获取相对路径部分 | "Users/me/data/report.txt" |
举个例子,假设你有一个路径"/home/user/docs/file.txt":
fs::path p("/home/user/docs/file.txt");
std::cout << "文件名: " << p.filename() << "\n";
std::cout << "主名: " << p.stem() << "\n";
std::cout << "扩展名: " << p.extension() << "\n";
std::cout << "父目录: " << p.parent_path() << "\n";
输出:
文件名: "file.txt"
主名: "file"
扩展名: ".txt"
父目录: "/home/user/docs"
你看,不用自己写字符串解析,干净利落。
4.3 目录遍历:递归与非递归
遍历目录是文件系统操作里的家常便饭。比如你要统计一个项目里有多少个.cpp文件,或者要批量重命名图片。
std::filesystem提供了两种遍历方式:
- 非递归遍历:只遍历当前目录,不进入子目录。
- 递归遍历:遍历所有子目录,直到最底层。
先看非递归的:
fs::path dir = "./data";
for (const auto& entry : fs::directory_iterator(dir)) {
std::cout << entry.path() << "\n";
}
这个只会列出./data下的直接内容。如果你要深入子目录,就用recursive_directory_iterator:
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) {
std::cout << "文件: " << entry.path() << "\n";
} else if (entry.is_directory()) {
std::cout << "目录: " << entry.path() << "\n";
}
}
这里有个细节:entry是一个directory_entry对象,它不光有路径,还缓存了文件状态。你可以直接调用is_regular_file()、is_directory()、file_size()等方法,不用再单独做一次系统调用。性能上会好一些。
recursive_directory_iterator的构造函数,传入directory_options::skip_permission_denied来跳过权限不足的目录,或者自己控制递归深度。
4.4 文件状态与常用操作
除了遍历,你还需要判断文件是否存在、获取文件大小、修改时间等。这些std::filesystem都提供了:
fs::path p = "test.txt";
// 判断是否存在
if (fs::exists(p)) {
std::cout << "文件存在\n";
}
// 获取文件大小
auto size = fs::file_size(p);
std::cout << "大小: " << size << " bytes\n";
// 获取最后修改时间
auto ftime = fs::last_write_time(p);
// 转换成时间戳(C++20有更优雅的方式)
auto cftime = decltype(ftime)::clock::to_time_t(ftime);
std::cout << "修改时间: " << std::ctime(&cftime);
创建和删除目录也很简单:
// 创建单级目录
fs::create_directory("new_folder");
// 创建多级目录(如果父目录不存在,会自动创建)
fs::create_directories("a/b/c/d");
// 删除空目录
fs::remove("empty_folder");
// 删除目录及其所有内容(危险操作!)
fs::remove_all("folder_to_delete");
std::filesystem的大多数函数在失败时会抛出std::filesystem::filesystem_error异常。如果你不想用异常,可以传入一个std::error_code参数来接收错误信息。
4.5 跨平台注意事项
虽然std::filesystem帮你屏蔽了大部分差异,但有些坑还是得自己注意:
- 路径大小写敏感:Windows不区分大小写,Linux区分。所以
"File.txt"和"file.txt"在Windows上是同一个文件,在Linux上不是。写跨平台代码时,最好统一用小写。 - 权限模型不同:Windows的权限和Linux的权限机制完全不同。
std::filesystem不提供修改权限的跨平台接口(C++20的permissions函数是实验性的)。如果你需要处理权限,得用平台特定的API。 - 符号链接:Windows的快捷方式和Linux的符号链接不是一回事。
std::filesystem的is_symlink()在Windows上只对真正的符号链接有效,对.lnk文件无效。
我个人建议,在项目初期就定好文件操作的封装层。把所有std::filesystem调用集中到一个模块里,这样万一以后要换底层实现(比如用boost::filesystem),改动范围也小。
4.6 知识结构图
下面这张图概括了本章的核心内容,你可以把它当作一个快速参考:
这张图把std::filesystem的核心功能分成了四大块:路径处理、目录遍历、文件操作和跨平台注意事项。你在实际项目中,基本上就是在这四个维度里打转。
4.7 一个完整的例子
最后,我给你一个完整的实战例子。假设你要写一个工具,统计某个目录下所有.cpp和.h文件的总行数:
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
namespace fs = std::filesystem;
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "用法: " << argv[0] << " <目录路径>\n";
return 1;
}
fs::path root = argv[1];
if (!fs::exists(root) || !fs::is_directory(root)) {
std::cerr << "无效的目录\n";
return 1;
}
size_t total_lines = 0;
size_t file_count = 0;
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (!entry.is_regular_file()) continue;
auto ext = entry.path().extension();
if (ext != ".cpp" && ext != ".h") continue;
std::ifstream file(entry.path());
std::string line;
size_t lines = 0;
while (std::getline(file, line)) {
++lines;
}
total_lines += lines;
++file_count;
std::cout << entry.path() << ": " << lines << " 行\n";
}
std::cout << "\n总计: " << file_count << " 个文件, "
<< total_lines << " 行代码\n";
return 0;
}
这个例子用到了本章讲的大部分知识点:路径拼接、扩展名判断、递归遍历、文件状态检查。你把它编译出来,在项目根目录跑一下,就能看到每个源文件的行数统计。
嗯,文件系统抽象这块,说白了就是让你少操心平台差异,多关注业务逻辑。用好std::filesystem,你的代码会干净很多,也更容易维护。