Cpp std::filesystem 学习笔记
关于 C++17 标准库 std::filesystem 的整理笔记。
概述
std::filesystem 是 C++17 引入的标准库模块,用于对文件和目录进行跨平台操作。
它提供了文件路径管理、目录操作、文件判断、遍历、创建和删除等功能,使用时无需依赖操作系统特定 API。
由于 Linux 系统和 Windows 系统的文件系统确实存在很多差异,虽然 std::filesystem 的设计目标是屏蔽平台差异,但实际使用中仍然会存在细微差异。
这里以 Linux 平台为主进行实验,Windows 平台会存在分隔符等差异。
在导入头文件时,一般对命名空间使用如下别名缩写
1 |
|
路径基础
std::filesystem 的核心类型是 std::filesystem::path,用于表示文件或目录的路径,它并不会具体指代相对路径或绝对路径,只是简化了一些对路径常用的字符串解析操作,然后再对接底层提供的文件系统操作。
创建路径对象
可以直接使用字符串创建路径
1 | std::filesystem::path p1("outputs/run1"); |
也可以从路径获取对应的字符串
1 | std::string s = p1.string(); // "outputs/run1" |
C++ 允许从字符串到路径类型的隐式转换,因此下面的很多操作的输入参数都可以直接使用字符串类型。
在进行文件读写操作时,可以直接使用路径类型,例如
1 | std::ofstream out(file); |
路径输出
路径类可以直接通过流输出,例如
1 | std::filesystem::path p1("outputs/run1"); |
输出形如
1 | "outputs/run1" |
对比可知,直接输出相比于转换为字符串再输出,会多出一对双引号。
路径信息
路径类不仅仅是一个字符串,还有其它的附属信息,可以通过如下方法获取
1 | std::filesystem::path dir = "outputs"; |
输出如下
1 | parent path: "outputs" |
注意这里的获取扩展名包括点号。
还可以获取文件的一些基本信息,例如文件的最后修改时间和文件大小
1 | auto ftime = std::filesystem::last_write_time(file); |
空路径判断
可以使用 empty() 方法判断路径是否为空(只是判断对应字符串是不是空,并不涉及路径是否存在)
1 | std::filesystem::path p; |
路径处理
当前工作路径
和其他语言一样,C++ 程序在启动时同样会有一个当前工作路径的概念,这会决定程序中所有相对路径的处理方式。
可以通过 std::filesystem::current_path() 获取当前工作路径,也可以通过 std::filesystem::current_path(path) 修改当前工作路径。
例如
1 |
|
解释一下这个程序会依次进行如下操作:
- 在当前位置创建一个目录
outputs/run1 - 创建一个文件
before.txt(使用相对路径,也就是在当前工作路径下) - 将当前工作路径修改为
outputs/run1 - 创建一个文件
after.txt(使用相对路径,也就是在outputs/run1目录下)
除此之外,还可以通过 std::filesystem::temp_directory_path() 获取一个系统提供的临时目录路径,可以用于存放缓存文件或临时输出,对于 Linux 通常为 /tmp,对于 Windows 通常为 %TEMP%。
路径拼接
对路径的常用操作是路径拼接,可以直接使用 / 运算符,例如
1 | std::filesystem::path dir = "outputs"; |
拼接操作在不同平台下会自动选择对应的路径分隔符:
- Linux 的拼接结果为
"outputs/data.csv" - Windows 的拼接结果为
"outputs\\data.csv"
如果在输出时希望无论 Linux 还是 Windows 都统一使用 /,可以在输出时通过 generic_string() 方法进行转换。
虽然重载的运算符
/就是路径分隔符,但是也有人不喜欢这个做法,觉得与/通常的语义矛盾,更应该使用+。
绝对路径和相对路径
一个最常见的需求是基于当前工作路径,把一个相对路径变成绝对路径,例如
1 | std::filesystem::current_path("/home/me/project"); |
路径规范化
路径的字符串形式可能存在 ../、./ 等不规范的表示,需要进行规范化处理。
std::filesystem::lexically_normal() 函数会对字符串进行简单的处理:
- 只会处理
.和..这种简单文本问题 - 不会去访问文件系统去检查路径是否存在等实际问题。
例如
1 | std::filesystem::path p = "a/b/../c/./d"; |
标准路径
有时我们需要通过文件系统获取真实可靠的绝对路径,此时可以使用 std::filesystem::canonical() 或 std::filesystem::weakly_canonical() 函数。前者更严格,会检查路径的所有部分,确保它们都存在。后者是更灵活,会尝试解析已有部分,忽略不存在的部分。
考虑下面的例子,需要处理相对路径 data/missing_dir/input.txt,其中 data 目录存在,但是 missing_dir 目录不存在。
1 |
|
输出如下
1 | canonical(bad) = canonical(bad) ERROR: filesystem error: cannot make canonical path: No such file or directory [data/missing_dir/input.txt] |
这表明 canonical() 函数在部分路径不存在时会直接抛出异常,而 weakly_canonical() 函数会尽量处理,返回的路径可能并不存在。
文件和目录操作
下面的很多函数其实都有两个版本,其中一个版本在操作失败时会抛异常,另一个版本多出一个错误码参数,操作失败会通过错误码返回错误信息。
例如
1 | bool create_directory(const std::filesystem::path& p); |
这里的操作失败可能有很多因素,例如权限不足或者文件被占用等,但是都超出了 C++ 程序本身可以控制的范畴。
判断存在性
使用下面的语句可以判断路径是否存在(包括文件或目录)
1 | if (std::filesystem::exists(file)) { ... } |
也可以使用如下方法来进行更精细的判断
1 | if (std::filesystem::is_regular_file(file)) { ... } |
除此之外,还有很多特殊的文件类型,例如符号链接、硬链接等,对于符号链接和硬链接,显然可以有多种处理方式,这里不做讨论。
创建新目录
至于创建新文件,直接写入新文件即可,不需要这里的文件系统库。
创建新目录的相关函数如下
1 | std::filesystem::create_directory("outputs/run1"); |
这两个函数的差异在于是否递归创建多级目录:
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 | for (auto &entry : std::filesystem::directory_iterator("outputs")) { |
两者的区分在于:
directory_iterator只遍历当前目录recursive_directory_iterator递归遍历子目录
