Cpp std::filesystem 学习笔记
关于 C++17 标准库 std::filesystem 的整理笔记。
概述
std::filesystem 是 C++17
引入的标准库模块,用于对文件和目录进行跨平台操作。
它提供了文件路径管理、目录操作、文件判断、遍历、创建和删除等功能,使用时无需依赖操作系统特定
API。
由于 Linux 系统和 Windows 系统的文件系统确实存在很多差异,虽然
std::filesystem
的设计目标是屏蔽平台差异,但实际使用中仍然会存在细微差异。 这里以 Linux
平台为主进行实验,Windows 平台会存在分隔符等差异。
在导入头文件时,一般对命名空间使用如下别名缩写 1
2
3
namespace fs = std::filesystem;
路径基础
std::filesystem 的核心类型是
std::filesystem::path,用于表示文件或目录的路径,它并不会具体指代相对路径或绝对路径,只是简化了一些对路径常用的字符串解析操作,然后再对接底层提供的文件系统操作。
创建路径对象
可以直接使用字符串创建路径 1
2std::filesystem::path p1("outputs/run1");
std::filesystem::path p2 = "outputs/run2";
也可以从路径获取对应的字符串 1
std::string s = p1.string(); // "outputs/run1"
C++ 允许从字符串到路径类型的隐式转换,因此下面的很多操作的输入参数都可以直接使用字符串类型。
在进行文件读写操作时,可以直接使用路径类型,例如 1
2
3std::ofstream out(file);
out << "x,y,z\n";
out.close();
路径输出
路径类可以直接通过流输出,例如 1
2
3std::filesystem::path p1("outputs/run1");
std::cout << p1 << '\n';
std::cout << p1.string() << '\n';
输出形如 1
2"outputs/run1"
outputs/run1
对比可知,直接输出相比于转换为字符串再输出,会多出一对双引号。
路径信息
路径类不仅仅是一个字符串,还有其它的附属信息,可以通过如下方法获取
1
2
3
4
5
6std::filesystem::path dir = "outputs";
std::filesystem::path file = dir / "data.csv";
std::cout << "parent path: " << file.parent_path() << '\n';
std::cout << "file name: " << file.filename() << '\n';
std::cout << "extension: " << file.extension() << '\n';
输出如下 1
2
3parent path: "outputs"
file name: "data.csv"
extension: ".csv"
注意这里的获取扩展名包括点号。
还可以获取文件的一些基本信息,例如文件的最后修改时间和文件大小
1
2auto ftime = std::filesystem::last_write_time(file);
auto size = std::filesystem::file_size(file);
空路径判断
可以使用 empty()
方法判断路径是否为空(只是判断对应字符串是不是空,并不涉及路径是否存在)
1
2
3
4std::filesystem::path p;
if (p.empty()) {
std::cout << "path is empty\n";
}
路径处理
当前工作路径
和其他语言一样,C++ 程序在启动时同样会有一个当前工作路径的概念,这会决定程序中所有相对路径的处理方式。
可以通过 std::filesystem::current_path()
获取当前工作路径,也可以通过
std::filesystem::current_path(path) 修改当前工作路径。
例如 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
34
int main() {
std::cout << "=== Before changing current_path ===\n";
auto cwd_before = std::filesystem::current_path();
std::cout << "current_path = " << cwd_before << "\n\n";
std::filesystem::create_directories("outputs/run1");
{
std::ofstream out("before.txt");
out << "created before changing current_path\n";
}
std::cout << "Changing current_path to outputs/run1 ...\n";
std::filesystem::current_path("outputs/run1");
std::cout << "=== After changing current_path ===\n";
auto cwd_after = std::filesystem::current_path();
std::cout << "current_path = " << cwd_after << "\n\n";
{
std::ofstream out("after.txt");
out << "created after changing current_path\n";
}
std::cout << "Files created:\n"
<< " - before.txt (in original CWD)\n"
<< " - after.txt (in outputs/run1)\n";
return 0;
}
解释一下这个程序会依次进行如下操作:
- 在当前位置创建一个目录
outputs/run1 - 创建一个文件
before.txt(使用相对路径,也就是在当前工作路径下) - 将当前工作路径修改为
outputs/run1 - 创建一个文件
after.txt(使用相对路径,也就是在outputs/run1目录下)
除此之外,还可以通过
std::filesystem::temp_directory_path()
获取一个系统提供的临时目录路径,可以用于存放缓存文件或临时输出,对于
Linux 通常为 /tmp,对于 Windows 通常为
%TEMP%。
路径拼接
对路径的常用操作是路径拼接,可以直接使用 / 运算符,例如
1
2std::filesystem::path dir = "outputs";
std::filesystem::path file = dir / "data.csv";
拼接操作在不同平台下会自动选择对应的路径分隔符:
- Linux 下拼接结果为
"outputs/data.csv" - Windows 下默认结果为
"outputs\\data.csv"
如果在输出时希望无论 Linux 还是 Windows 都统一使用
/,可以在输出时通过 generic_string()
方法进行转换。
绝对路径和相对路径
一个最常见的需求是基于当前工作路径,把一个相对路径变成绝对路径,例如
1
2
3std::filesystem::current_path("/home/me/project");
std::cout << std::filesystem::absolute("data/output");
// "/home/me/project/data/output"
路径规范化
路径的字符串形式可能存在 ../、./
等不规范的表示,需要进行规范化处理。
std::filesystem::lexically_normal()
函数会对字符串进行简单的处理:
- 只会处理
.和..这种简单文本问题 - 不会去访问文件系统去检查路径是否存在等实际问题。
例如 1
2std::filesystem::path p = "a/b/../c/./d";
std::cout << p.lexically_normal(); // "a/c/d"
标准路径
有时我们需要通过文件系统获取真实可靠的绝对路径,此时可以使用
std::filesystem::canonical() 或
std::filesystem::weakly_canonical()
函数。前者更严格,会检查路径的所有部分,确保它们都存在。后者是更灵活,会尝试解析已有部分,忽略不存在的部分。
考虑下面的例子,需要处理相对路径
data/missing_dir/input.txt,其中 data
目录存在,但是 missing_dir 目录不存在。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main() {
std::filesystem::path bad = "data/missing_dir/input.txt";
try {
std::cout << "canonical(bad) = " << std::filesystem::canonical(bad) << '\n';
}
catch (const std::filesystem::filesystem_error &e) {
std::cout << "canonical(bad) ERROR: " << e.what() << '\n';
}
std::cout << "weakly_canonical(bad) = " << std::filesystem::weakly_canonical(bad)
<< '\n';
}
输出如下 1
2canonical(bad) = canonical(bad) ERROR: filesystem error: cannot make canonical path: No such file or directory [data/missing_dir/input.txt]
weakly_canonical(bad) = "/home/xxx/test/data/missing_dir/input.txt"
这表明 canonical()
函数在部分路径不存在时会直接抛出异常,而 weakly_canonical()
函数会尽量处理,返回的路径可能并不存在。
文件和目录操作
下面的很多函数其实都有两个版本,其中一个版本在操作失败时会抛异常,另一个版本多出一个错误码参数,操作失败会通过错误码返回错误信息。
例如 1
2bool create_directory(const std::filesystem::path& p);
bool create_directory(const std::filesystem::path& p, std::error_code& ec) noexcept;
这里的操作失败可能有很多因素,例如权限不足或者文件被占用等,但是都超出了 C++ 程序本身可以控制的范畴。
判断存在性
使用下面的语句可以判断路径是否存在(包括文件或目录)
1
if (std::filesystem::exists(file)) { ... }
也可以使用如下方法来进行更精细的判断 1
2
3if (std::filesystem::is_regular_file(file)) { ... }
if (std::filesystem::is_directory(dir)) { ... }
除此之外,还有很多特殊的文件类型,例如符号链接、硬链接等,对于符号链接和硬链接,显然可以有多种处理方式,这里不做讨论。
创建新目录
至于创建新文件,直接写入新文件即可,不需要这里的文件系统库。
创建新目录的相关函数如下 1
2
3std::filesystem::create_directory("outputs/run1");
std::filesystem::create_directories("outputs/run2/a/b");
这两个函数的差异在于是否递归创建多级目录:
std::filesystem::create_directory只能创建一个目录,要求它的父目录必须存在;std::filesystem::create_directories可以自动创建缺少的中间各级目录。
注意:如果要创建的目录已经存在,会跳过操作,这不会被视作错误。
删除文件和目录
删除单个文件或空目录 1
std::filesystem::remove(file);
删除文件或递归地删除目录以及它的所有子目录和其中的内容
1
std::filesystem::remove_all(dir);
复制和移动文件
文件或目录的复制,例如 1
std::filesystem::copy("src.txt", "dst.txt");
注意:
- 如果目标文件已存在,则会被直接覆盖。
- 如果目标位置的父文件夹不存在,则会导致错误,抛出异常。
- 复制文件夹时的行为取决于第三个选项,可能递归或非递归。
文件或目录的重命名(或移动) 1
std::filesystem::rename("old.txt", "new.txt");
关于单个文件的复制,有如下更精细的操作接口 1
std::filesystem::copy_file("src.txt", "dst.txt", std::filesystem::copy_options::overwrite_existing);
第三个参数通常有:
copy_options::overwrite_existing:如果目标文件存在,直接覆盖copy_options::skip_existing:如果目标文件存在,直接跳过copy_options::update_existing:如果目标文件存在,且目标文件的修改时间比源文件旧,则用源文件进行更新
目录遍历
可以使用下面的迭代器进行目录遍历,使用 path()
方法获取文件路径 1
2
3
4
5
6
7for (auto &entry : std::filesystem::directory_iterator("outputs")) {
std::cout << entry.path() << '\n';
}
for (auto &entry : std::filesystem::recursive_directory_iterator("outputs")) {
std::cout << entry.path() << '\n';
}
两者的区分在于:
directory_iterator只遍历当前目录recursive_directory_iterator递归遍历子目录
