C++17 详解 23 —文件系统

本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。

翻译太特么累人了……剩余部分还是只做摘要翻译吧。

12. 文件系统

12.1 总览

头文件:

1
#include <filesystem>

std::filesystem 是一个模块,同时也是一个命名空间。核心元素包括:

  • std::filesystem::path 对象允许你操作代表存在或不存在的文件和目录的路径。
  • std::filesystem::directory_entry 表示一个存在的路径,并附带额外状态信息,比如最后修改时间、文件尺寸以及其它属性。
  • 目录迭代器允许你遍历一个给定的目录。库提供了递归和非递归版本。
  • 大量支持函数比如获取路径信息、文件操作、权限、创建目录等等。

12.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
34
35
36
37
38
#include <filesystem>
#include <iomanip>
#include <iostream>

namespace fs = std::filesystem;

void DisplayDirectoryTree(const fs::path &pathToScan, int level = 0) {
for (const auto &entry : fs::directory_iterator(pathToScan)) {
const auto filenameStr = entry.path().filename().string();
if (entry.is_directory()) {
std::cout << std::setw(level * 3) << "" << filenameStr << '\n';
DisplayDirectoryTree(entry, level + 1);
}
else if (entry.is_regular_file()) {
std::cout << std::setw(level * 3) << "" << filenameStr
<< ", size " << fs::file_size(entry) << " bytes\n";
}
else
std::cout << std::setw(level * 3) << "" << " [?]" << filenameStr << '\n';
}
}
int main(int argc, char *argv[]) {
try {
const fs::path pathToShow{ argc >= 2 ? argv[1] : fs::current_path() };

std::cout << "listing files in the directory: "
<< fs::absolute(pathToShow).string() << '\n';

DisplayDirectoryTree(pathToShow);
}
catch (const fs::filesystem_error &err) {
std::cerr << "filesystem error! " << err.what() << '\n';

}
catch (const std::exception &ex) {
std::cerr << "general exception: " << ex.what() << '\n';
}
}

Windows 下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.\ListFiles.exe D:\testlist\
listing files in the directory: D:\testlist\
abc.txt, size 357 bytes
def.txt, size 430 bytes
ghi.txt, size 190 bytes
dirTemp
jkl.txt, size 162 bytes
mno.txt, size 1728 bytes
tempDir
abc.txt, size 174 bytes
def.txt, size 163 bytes
tempInner
abc.txt, size 144 bytes
mno.txt, size 1728 bytes
xyz.txt, size 3168 bytes

所有类型、函数和名字都存在于 std::filesystem 命名空间内,为了方便用到了命名空间别名(namespace alias):

1
namespace fs = std::filesystem;

main 函数中:

程序接受一个来自命令行的可选参数,如果为空则使用当前系统路径:

1
const fs::path pathToShow{ argc >= 2 ? argv[1] : fs::current_path() };

fs::absolute() 函数将输入路径转换成绝对路径。

DisplayDirectoryTree 函数中:

使用 directory_iterator 检查目录并查找(子)文件或(子)目录:

1
for (const auto& entry : fs::directory_iterator(pathToShow))

每次迭代都返回一个新的 fs::directory_entry 对象。

通过 entry.is_directory() 判断当前项是否是子目录,如果是则递归调用 DisplayDirectoryTree。

12.3 path 对象

std::filesystem::path 是库的核心构成。

path 由下列元素组成:

root-name root-directory relative-path

+(可选)root-name:POSIX 系统没有根名称。Windows 上它通常是驱动器名称,像“C:”

  • (可选)root-directory:区分相对路径和绝对路径
  • relative-path:
    • filename
    • (可选)directory separator
    • (可选)relative-path

path 类实现了许多函数以提取路径中的各个部分:

如果某部分在路径中不存在,上述函数会返回一个空 path 对象(path::empty() = true)。

同时,上述函数都配有一个 bool has_xxx() 的查询函数。

示例:

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
const filesystem::path testPath{ ... };
if (testPath.has_root_name())
cout << "root_name() = " << testPath.root_name() << '\n';
else
cout << "no root-name\n";
if (testPath.has_root_directory())
cout << "root directory() = " << testPath.root_directory() << '\n';
else
cout << "no root-directory\n";
if (testPath.has_root_path())
cout << "root_path() = " << testPath.root_path() << '\n';
else
cout << "no root-path\n";
if (testPath.has_relative_path())
cout << "relative_path() = " << testPath.relative_path() << '\n';
else
cout << "no relative-path\n";
if (testPath.has_parent_path())
cout << "parent_path() = " << testPath.parent_path() << '\n';
else
cout << "no parent-path\n";
if (testPath.has_filename())
cout << "filename() = " << testPath.filename() << '\n';
else
cout << "no filename\n";
if (testPath.has_stem())
cout << "stem() = " << testPath.stem() << '\n';
else
cout << "no stem\n";
if (testPath.has_extension())
cout << "extension() = " << testPath.extension() << '\n';
else
cout << "no extension\n";

输入为 “C:\Windows\system.ini” 时的输出:

1
2
3
4
5
6
7
8
root_name() = "C:"
root directory() = "\\"
root_path() = "C:\\"
relative_path() = "Windows\\system.ini"
parent_path() = "C:\\Windows"
filename() = "system.ini"
stem() = "system"
extension() = ".ini"

POSIX 系统上,输入为 “/usr/temp/abc.txt” 时的输出:

1
2
3
4
5
6
7
8
no root-name
root directory() = "/"
root_path() = "/"
relative_path() = "usr/temp/abc.txt"
parent_path() = "/usr/temp"
filename() = "abc.txt"
stem() = "abc"
extension() = ".txt"

std::filesystem::path 同时还实现了 begin() 和 end() 函数,所以你可以在 range based for 循环里使用 path:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main()
{
const fs::path p = "C:\\users\\abcdef\\AppData\\Local\\Temp\\";
std::cout << "Examining the path " << p << " through iterators gives\n";
for (auto it = p.begin(); it != p.end(); ++it)
std::cout << *it << " │ ";
std::cout << '\n';
}

输出:

1
2
Examining the path "C:\users\abcdef\AppData\Local\Temp\" through iterators gives
"C:" │ "/" │ "users" │ "abcdef" │ "AppData" │ "Local" │ "Temp" │ "" │

译注:我以一个多层子目录的路径示例替换了原书示例。上边代码来自 cppreference

12.3.1 path 操作

其它重要函数:

比较

除 path::compare() 函数外,还支持操作符比较:

1
== != < > <= >=

所有方法均以路径的原生格式逐元素比较。

1
2
3
4
5
6
7
8
9
fs::path p1 { "/usr/a/b/c" };
fs::path p2 { "/usr/a/b/c" };
assert(p1 == p2);
assert(p1.compare(p2) == 0);

p1 = "/usr/a/b/c";
p2 = "/usr/a/b/c/d";
assert(p1 < p2);
assert(p1.compare(p2) < 0);

Windows 上还能测试带 root path 的路径:

1
2
3
4
p1 = "C:/test";
p2 = "abc/xyz"; // 没有 root path, 所以它“小于”有 root 的 path
assert(p1 > p2);
assert(p1.compare(p2) > 0);

同样能处理 Windows 上以不同格式表示的 path:

1
2
3
4
fs::path p3 { "/usr/a/b/c" }; // Windows 上它被转换为原生格式
fs::path p4 { "\\usr/a\\b/c" };
assert(p3 == p4);
assert(p3.compare(p4) == 0);

路径组合

  1. path::append()

添加一个目录分隔符和路径。

同样支持操作符:/ /=

1
2
3
4
fs::path p1{"C:\\temp"};
p1 /= "user";
p1 /= "data";
cout << p1 << '\n'; // C:\temp\user\data
  1. path::concat()

只添加 path 的字符串而不带目录分隔符。

操作符形式:+ +=

1
2
3
4
fs::path p2("C:\\temp\\");
p2 += "user";
p2 += "data";
cout << p2 << '\n'; // C:\temp\userdata

如果传入 path 带有 root-name,且 root-name 跟当前 path 的 root-name 不同,append 操作会替换当前路径:

1
2
3
4
auto resW = fs::path{"foo"} / "D:\"; // Windows
auto resP = fs::path{"foo"} / "/bar"; // POSIX
// resW 现在是 "D:\"
// resP 现在是 "/bar"

流操作符

path 类实现了 operator >> 和 operator <<。

操作符里用到了 std::quoted 以保存正确的格式。这会导致在 Windows 上以原生格式输出 path 中的 “\”。

比如 POSIX 上:

1
2
fs::path p1 { "/usr/test/temp.xyz" };
std::cout << p1;

会输出 "/usr/test/temp.xyz"(译注:带引号输出),

Windows 上:

1
2
fs::path p2{ "usr\\test\\temp.xyz" };
std::cout << p2;

会输出:"usr\\test\\temp.xyz"(译注:带引号,同时保留了 “\”)。

路径格式和转换

路径格式有两种:

  • 通用(generic)格式,标准格式(基于 POSIX 格式)
  • 原生(native)格式,某些特别的实现所用

路径格式在 Windows 和 POSIX 系统上的表现是不一样的。

  1. POSIX 系统上两种格式是一样的;Windows 上不一样。

  2. Windows 上使用反斜杠(\),且 Windows 上有 root-name,比如 C: D: 等。

  3. POSIX 系统上以 char 和 std::string 存储路径字符;Windows 上用的是 wchar_t 和 std::wstring。

原生格式接口:

从原生格式转换接口:

12.4 directory_entry 和 directory_iterator

path 可以表示一个存在或不存在的文件或路径,directory_entry 和 directory_iterator 指向存在的文件或目录。

使用 directory_iterators 遍历路径

遍历顺序都是不确定的。

如果迭代器被创建后,迭代器指向的目录树中有文件或目录被添加或删除,标准没有规范迭代器是否已知这种变化(译注:即未定义行为)。

“.” 和 “..” 这两个特殊目录会被迭代器跳过。

directory_entry (常用)方法

12.5 (常用)支持函数

查询类:

路径相关:

目录和文件管理:

12.6 错误处理 & 文件竞争(race)

  1. 所有 filesystem 库函数均可以用异常处理错误,同时所有函数都有一个带 error_code 参数的重载版本,可以通过 error_code 判断错误。

  2. 所有 filesystem 操作都是非线程安全的。

评论