Filesystem #
filesystem是C++17新增的一个标准库组件。从Boost.Filesystem移植而来,该库根据新语言标准进行了适配调整,并与标准库的其他组件保持统一。
它提供了文件系统中很多实用的功能:
操作文件系统路径(核心)创建、移动、重命名、删除目录以及列出指定目录内容- `列出给定目录的内容
获取路径本身或路径中文件的信息查询或设置文件权限
但需要注意的是文件的读写操作仍然是需要文件流中相关的类(ofstream、ifstream和fstream)
filesystem的API通过头文件<filesystem>提供,并且位于std::filesystem命令空间内
path:这个类类型是filesystem中最常用的组件,它用于操作表示现有文件或目录的路径,它不代表任何实体,这个实体哪怕不存在也可以使用path类表示,它只是一个表示路径的类型,不是实体directory_entry:这个类类型的对象包含路径及其附加信息(文件大小、时间戳等)directory_iterator:用于遍历目录内容的迭代器- 还有一些其他的辅助函数和组件可简化文件系统操作
文件系统函数通过两种机制报告错误:
抛出std::filesystem_error异常返回错误码
通常,这些可能发生错误的函数会有两个版本,这两个版本的函数是重载关系。一个版本的函数抛出一场,另外一个版本返回错误码。
path类 #
通过代码示例来去理解filesystem各个组件和函数之间的关系
使用path类创建一个实例,初始化内容可以是文件系统中某个文件或目录的路径,记住是路径,一个字符串而已
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #include <iostream>
#include <filesystem>
void using_path() {
namespace fs = std::filesystem;
fs::path sel_path{"/home/parallels/bqe/data/filesystem.cc"};
std::cout << "sel_path: " << sel_path << "\n";
// 通用格式显示 windows平台上不会显示两个\\
std::cout << "sel_path.string(): " << sel_path.string() << "\n";
}
int main() {
using_path();
return 0;
}
|
输出结果示例:
1
2
| sel_path: "/home/parallels/bqe/data/filesystem.cc"
sel_path.string(): /home/parallels/bqe/data/filesystem.cc
|
获取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
35
36
37
38
39
40
41
42
43
| #include <iostream>
#include <filesystem>
void using_path() {
namespace fs = std::filesystem;
fs::path sel_path{"/home/parallels/bqe/data/filesystem.cc"};
std::cout << "sel_path: " << sel_path << "\n";
std::cout << "sel_path.string(): " << sel_path.string() << "\n";
// 根名称
if (sel_path.has_root_name())
std::cout << "root name\t = " << sel_path.root_name().string() << "\n";
// 根路径
if (sel_path.has_root_path())
std::cout << "root path\t = " << sel_path.root_path().string() << "\n";
// 根目录
if (sel_path.has_root_directory())
std::cout << "root directory\t = " << sel_path.root_directory().string() << "\n";
// 父目录
if (sel_path.has_parent_path()) {
std::cout << "parent path\t = " << sel_path.parent_path().string() << "\n";
}
// 相对路径
if (sel_path.has_relative_path())
std::cout << "relative path\t = " << sel_path.relative_path().string() << "\n";
// 文件名称
if (sel_path.has_filename())
std::cout << "filename\t = " << sel_path.filename().string() << "\n";
if (sel_path.has_stem()) std::cout << "stem part \t = " << sel_path.stem().string() << "\n";
// 扩展名
if (sel_path.has_extension())
std::cout << "extension\t = " << sel_path.extension().string() << "\n";
}
int main() {
using_path();
return 0;
}
|
输出结果:
1
2
3
4
5
6
7
8
9
| sel_path: "/home/parallels/bqe/data/filesystem.cc"
sel_path.string(): /home/parallels/bqe/data/filesystem.cc
root path = /
root directory = /
parent path = /home/parallels/bqe/data
relative path = home/parallels/bqe/data/filesystem.cc
filename = filesystem.cc
stem part = filesystem
extension = .cc
|
- 注意,在Linux/Unix系统中,root name的名称永远为空,因为在这类系统下没有盘符的概念,它们只有一个根,那就是/。而在windows中有盘符的概念,例如C或者E等等,然后根路径就是E:\,根目录就是\。和类unix系统不一样的。
- path类型下的成员函数relative_path是去掉根目录之后的子路径,比如/a/b/c 那么子路径就是a/b/c。与计算相对路径的relative是不一样的,relative是计算路径A相对于B的相对关系,比如/a/b/c这个路径相对于/a得到的结果就是b/c。relative(p,base)->意思就是从base出发到p的路径该如何写,模仿一下cd命令即可。要注意的是relative的两个参数必须是相对路径或者都是绝对路径,否则会抛出异常
- 上述的成员函数的返回类型都是path,值得注意
- 值得注意的是这目前还没操作任何目录项(实际存在的目录、文件)
修改path类 #
filesystem还提供修改path对象的内容,比如上述的path对象中的内容是"/home/parallels/bqe/data/filesystem.cc",你可以选择去掉后面的文件名,使这个路径对象的内容是"/home/parallels/bqe/data/",看一下修改之后的示例
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
39
40
41
42
43
44
45
46
47
48
49
50
| #include <iostream>
#include <filesystem>
void using_path() {
namespace fs = std::filesystem;
fs::path sel_path{"/home/parallels/bqe/data/filesystem.cc"};
std::cout << "sel_path: " << sel_path << "\n";
std::cout << "sel_path.string(): " << sel_path.string() << "\n";
std::cout
<< "--------------------------after removing filename-----------------------------------\n";
// 修改path对象,将文件名去掉
sel_path.remove_filename();
std::cout << "sel_path: " << sel_path << "\n";
std::cout << "sel_path.string(): " << sel_path.string() << "\n";
// 根名称
if (sel_path.has_root_name())
std::cout << "root name\t = " << sel_path.root_name().string() << "\n";
// 根路径
if (sel_path.has_root_path())
std::cout << "root path\t = " << sel_path.root_path().string() << "\n";
// 根目录
if (sel_path.has_root_directory())
std::cout << "root directory\t = " << sel_path.root_directory().string() << "\n";
// 父目录
if (sel_path.has_parent_path()) {
std::cout << "parent path\t = " << sel_path.parent_path().string() << "\n";
}
// 相对路径
if (sel_path.has_relative_path())
std::cout << "relative path\t = " << sel_path.relative_path().string() << "\n";
// 文件名称
if (sel_path.has_filename())
std::cout << "filename\t = " << sel_path.filename().string() << "\n";
if (sel_path.has_stem()) std::cout << "stem part \t = " << sel_path.stem().string() << "\n";
// 扩展名
if (sel_path.has_extension())
std::cout << "extension\t = " << sel_path.extension().string() << "\n";
}
int main() {
using_path();
return 0;
}
|
查看输出信息:
1
2
3
4
5
6
7
8
9
| sel_path: "/home/parallels/bqe/data/filesystem.cc"
sel_path.string(): /home/parallels/bqe/data/filesystem.cc
--------------------------after removing filename-----------------------------------
sel_path: "/home/parallels/bqe/data/"
sel_path.string(): /home/parallels/bqe/data/
root path = /
root directory = /
parent path = /home/parallels/bqe/data
relative path = home/parallels/bqe/data/
|
此时path对象的内容就只是一个简单的目录路径了,不带有文件名
移除文件名之后,还想在path对象中添加一个新的路径信息该如何?还是上面的示例,使用append成员函数或者直接使用/=运算符添加一个bitofux.cc,也可添加一个目录
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| #include <iostream>
#include <filesystem>
void using_path() {
namespace fs = std::filesystem;
fs::path sel_path{"/home/parallels/bqe/data/filesystem.cc"};
std::cout << "sel_path: " << sel_path << "\n";
std::cout << "sel_path.string(): " << sel_path.string() << "\n";
std::cout
<< "--------------------------after removing filename-----------------------------------\n";
// 修改path对象,将文件名去掉
sel_path.remove_filename();
std::cout << "sel_path: " << sel_path << "\n";
std::cout << "sel_path.string(): " << sel_path.string() << "\n";
// 添加新的信息,比如文件名:bitofux.cc
sel_path.append("bitofux.cc");
// 或者使用
// sel_path /= "bitofux";
// 根名称
if (sel_path.has_root_name())
std::cout << "root name\t = " << sel_path.root_name().string() << "\n";
// 根路径
if (sel_path.has_root_path())
std::cout << "root path\t = " << sel_path.root_path().string() << "\n";
// 根目录
if (sel_path.has_root_directory())
std::cout << "root directory\t = " << sel_path.root_directory().string() << "\n";
// 父目录
if (sel_path.has_parent_path()) {
std::cout << "parent path\t = " << sel_path.parent_path().string() << "\n";
}
// 相对路径
if (sel_path.has_relative_path())
std::cout << "relative path\t = " << sel_path.relative_path().string() << "\n";
// 文件名称
if (sel_path.has_filename())
std::cout << "filename\t = " << sel_path.filename().string() << "\n";
if (sel_path.has_stem()) std::cout << "stem part \t = " << sel_path.stem().string() << "\n";
// 扩展名
if (sel_path.has_extension())
std::cout << "extension\t = " << sel_path.extension().string() << "\n";
}
int main() {
using_path();
return 0;
}
|
查看输出信息:
1
2
3
4
5
6
7
8
9
10
11
12
| sel_path: "/home/parallels/bqe/data/filesystem.cc"
sel_path.string(): /home/parallels/bqe/data/filesystem.cc
--------------------------after removing filename-----------------------------------
sel_path: "/home/parallels/bqe/data/"
sel_path.string(): /home/parallels/bqe/data/
root path = /
root directory = /
parent path = /home/parallels/bqe/data
relative path = home/parallels/bqe/data/bitofux.cc
filename = bitofux.cc
stem part = bitofux
extension = .cc
|
filesystem-directory-entry #
定义一个函数,参数的类型是string_view,这个函数的功能完成对传入参数代表的路径下的目录项进行遍历。
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
| #include <iostream>
#include <filesystem>
#include <string_view>
namespace fs = std::filesystem;
void list_directory(std::string_view file) {
fs::path currentPath{file};
// 输出当前路径的字符串
std::cout << "currentPath: " << currentPath.string() << "\n";
// 使用fs::director_iterator这个类型定义两个对象
// 分别表示路径下的起始位置和结束位置
// 迭代器指向directory_entry对象,可以使用其访问
// 每个目录项的详细信息
// 使用当前路径对象进行初始化获取起始位置
fs::directory_iterator begin{currentPath};
// 使用空参数进行初始化获取最后一个目录项的下一个位置
fs::directory_iterator end{};
while (begin != end) {
auto de = *begin;
if (de.is_directory()) {
std::cout << "directory: " << de.path().string() << "\n";
} else {
std::cout << "filename: " << de.path().filename().string() << "\n";
}
++begin;
}
}
int main() {
list_directory("./");
return 0;
}
|
directory_entry:是路径下的目录项的类型,这个类型定义的对象代表一个真正的目录项实体directory_iterator:一个迭代器类型,可以指向一个目录项实体,使用一个path对象初始化,获取的是一个指向这个path对象对应路径下的第一个目录项实体的迭代器,空参数获取的指向最后一个有效实体的后继位置的迭代器,利用这两者的不等关系,可以遍历此path对象下的目录项实体director_iterator支持前自增和后自增运算符还有\*运算符de代表的就是指向的其中一个目录项,这个类型下有很多成员函数来判断目录项的类型,是否为目录、是否为普通文件等等
输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| currentPath: ./
filename: 03_test.cc
filename: 04_test.cc
filename: 12_test.cc
filename: list_dir.cc
filename: chrono.cc
filename: 08_test.cc
directory: ./xcj_thread
filename: 01_test.cc
filename: 02_test.cc
directory: ./11_test
filename: a.out
filename: 10_test.cc
filename: 06_test.cc
filename: cpp_string.cc
filename: 09_test.cc
filename: 07_test.cc
filename: 05_test.cc
|
但是使用while循环的方式很容易出错,比如忘记自增等等,你可以将一个路径看作是一个容器,里面存储的都是若干个目录项,因此,是可以使用基于范围的for循环的
==使用基于范围的for循环,遍历当前路径,并根据目录项的不同类型对应不同的输出结构==
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
| #include <iostream>
#include <filesystem>
#include <string_view>
void list_directory_entry(std::string_view file) {
fs::path currentPath{file};
if (!fs::exists(currentPath)) {
std::cerr << "currentPath is invalid\n";
return;
}
// 基于范围的for循环
for (const auto& dir_entry : fs::directory_iterator{currentPath}) {
// 获取当前目录项实体的类型枚举值
// 使用switch进行枚举,判断是否是目录或者普通文件
// dir_entry.status() -> file_status
// dir_entry.status().type() -> file_type
switch (dir_entry.status().type()) {
case fs::file_type::regular: {
std::cout << "filename: " << dir_entry.path().filename().string()
<< " file size: " << dir_entry.file_size() << "\n";
break;
}
case fs::file_type::directory: {
std::cout << "DIR: " << fs::relative(dir_entry.path(), currentPath).string()
<< "\n";
break;
}
}
}
}
int main() {
// list_directory("./");
list_directory_entry("./");
return 0;
}
|
dir_entry.status()获取的目录项的状态,在调用type()获取的是file_type枚举类型的枚举值,可以利用switch对文件系统下的所有枚举类型进行比较,如果是普通文件输出普通文件的名称和文件大小,如果是目录,获取目录项当前所在的路径对象,利用路径对象与cuurrentPath计算当前路径对象相对于currentPath的相对路径并获取它的字符串
输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| filename: 03_test.cc file size: 556
filename: 04_test.cc file size: 644
filename: 12_test.cc file size: 3229
filename: list_dir.cc file size: 2366
filename: chrono.cc file size: 1784
filename: 08_test.cc file size: 1343
DIR: xcj_thread
filename: 01_test.cc file size: 1049
filename: 02_test.cc file size: 434
DIR: 11_test
filename: a.out file size: 87616
filename: 10_test.cc file size: 1722
filename: 06_test.cc file size: 3383
filename: cpp_string.cc file size: 2030
filename: 09_test.cc file size: 2016
filename: 07_test.cc file size: 2594
filename: 05_test.cc file size: 686
|
但是你会发现输出的内容的顺序是非常乱的,下面可以通过一些算法,使得目录在前,因为directory_entry类型和path类型都是支持比较运算符的,因此可以将目录项存储到vector中,这样就可以利用容器的排序算法对容器内的元素进行排序
使用分区算法(std::partition)的方案对容器内的元素进行排序,分区算法的思想是根据谓词条件对范围内的元素重新排序,满足谓词条件的元素被移动到容器前端
将是否为目录类型当作谓词条件,满足的元素会被放到容器前端
在文件系统中,可根据你的需求或持有对象的类型,可选择使用成员函数或全局函数二者之一。
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 <algorithm>
#include <iostream>
#include <filesystem>
#include <string_view>
#include <vector>
namespace fs = std::filesystem;
void use_partition(std::string_view file) {
fs::path currentPath{file};
if (!fs::exists(currentPath)) {
std::cerr << "currentPath is invalid\n";
return;
}
std::vector<fs::directory_entry> entrys{20};
for (const auto& dir_entry : fs::directory_iterator{currentPath}) {
entrys.push_back(dir_entry);
}
std::partition(entrys.begin(), entrys.end(),
[](const fs::directory_entry& de) { return de.is_directory(); });
for (const auto& elemnt : entrys) {
switch (elemnt.status().type()) {
case fs::file_type::regular: {
std::cout << "filename: " << elemnt.path().filename().string()
<< " file size: " << elemnt.file_size() << "\n";
break;
}
case fs::file_type::directory: {
std::cout << "DIR: " << fs::relative(elemnt.path(), currentPath).string() << "\n";
break;
}
}
}
}
int main() {
use_partition("./");
return 0;
}
|
查看输出结果:可以看到,目录都被移动到了前面,文件则在后面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| DIR: 11_test
DIR: xcj_thread
filename: 03_test.cc file size: 556
filename: 04_test.cc file size: 644
filename: 12_test.cc file size: 3229
filename: list_dir.cc file size: 3440
filename: chrono.cc file size: 1784
filename: 08_test.cc file size: 1343
filename: 01_test.cc file size: 1049
filename: 02_test.cc file size: 434
filename: a.out file size: 99336
filename: 10_test.cc file size: 1722
filename: 06_test.cc file size: 3383
filename: cpp_string.cc file size: 2030
filename: 09_test.cc file size: 2016
filename: 07_test.cc file size: 2594
filename: 05_test.cc file size: 686
|