Skip to main content

C++17下的文件系统库

·8 mins

Filesystem #

filesystem是C++17新增的一个标准库组件。从Boost.Filesystem移植而来,该库根据新语言标准进行了适配调整,并与标准库的其他组件保持统一。 它提供了文件系统中很多实用的功能:

  1. 操作文件系统路径(核心)
  2. 创建、移动、重命名、删除目录以及列出指定目录内容
  3. `列出给定目录的内容
  4. 获取路径本身或路径中文件的信息
  5. 查询或设置文件权限 但需要注意的是文件的读写操作仍然是需要文件流中相关的类(ofstream、ifstream和fstream)

filesystem的API通过头文件<filesystem>提供,并且位于std::filesystem命令空间内

  • path:这个类类型是filesystem中最常用的组件,它用于操作表示现有文件或目录的路径,它不代表任何实体,这个实体哪怕不存在也可以使用path类表示,它只是一个表示路径的类型,不是实体
  • directory_entry:这个类类型的对象包含路径及其附加信息(文件大小、时间戳等)
  • directory_iterator:用于遍历目录内容的迭代器
  • 还有一些其他的辅助函数和组件可简化文件系统操作

文件系统函数通过两种机制报告错误:

  1. 抛出std::filesystem_error异常
  2. 返回错误码 通常,这些可能发生错误的函数会有两个版本,这两个版本的函数是重载关系。一个版本的函数抛出一场,另外一个版本返回错误码。

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
  1. 注意,在Linux/Unix系统中,root name的名称永远为空,因为在这类系统下没有盘符的概念,它们只有一个根,那就是/。而在windows中有盘符的概念,例如C或者E等等,然后根路径就是E:\,根目录就是\。和类unix系统不一样的。
  2. 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的两个参数必须是相对路径或者都是绝对路径,否则会抛出异常
  3. 上述的成员函数的返回类型都是path,值得注意
  4. 值得注意的是这目前还没操作任何目录项(实际存在的目录、文件)

修改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