C++ `std::filesystem` (C++17) 深度:跨平台文件系统操作

哈喽,各位好!

今天咱们来聊聊 C++17 引入的 std::filesystem,这个库简直就是文件系统操作的一把瑞士军刀,让咱们在 C++ 里也能像玩泥巴一样轻松地摆弄文件和目录。

一、告别老古董:为什么我们需要 std::filesystem

在 C++17 之前,咱们操作文件系统,要么用 C 标准库的 stdio.h (比如 fopen, fclose, fread, fwrite 这些),要么用平台特定的 API(比如 Windows 的 CreateFile, ReadFile,Linux 的 open, read)。

这些方法问题多多:

  1. 平台依赖性高: 同一段代码,在 Windows 上跑得欢,到了 Linux 上就歇菜了。跨平台?不存在的。
  2. 错误处理繁琐: 动不动就要检查返回值,errno,各种宏定义,头都大了。
  3. 功能有限: 创建目录、遍历目录这些常见操作,实现起来都比较麻烦。

std::filesystem 的出现,就是为了解决这些痛点。它提供了一套标准的、跨平台的、面向对象的文件系统操作接口,让咱们的代码更简洁、更易维护、更具可移植性。

二、std::filesystem 的核心概念

std::filesystem 库的核心类是 std::filesystem::path。可以把它理解为文件或目录的路径,它提供了一系列方法来操作路径,比如拼接、分解、判断存在等等。

除了 path,还有一些其他的类,比如 file_status (文件状态),directory_entry (目录项),space_info (空间信息)等等。

三、std::filesystem 的基本用法

下面咱们通过一些例子来学习 std::filesystem 的用法。

  1. 路径操作

    #include <iostream>
    #include <filesystem>
    
    namespace fs = std::filesystem; // 方便使用
    
    int main() {
       fs::path p1 = "/home/user/documents/report.txt"; // 绝对路径
       fs::path p2 = "data/input.csv"; // 相对路径
    
       std::cout << "Path 1: " << p1 << std::endl;
       std::cout << "Path 2: " << p2 << std::endl;
    
       // 拼接路径
       fs::path p3 = p1 / "backup"; // 相当于 /home/user/documents/report.txt/backup
       std::cout << "Path 3 (拼接): " << p3 << std::endl;
    
       // 获取文件名
       std::cout << "Filename: " << p1.filename() << std::endl; // report.txt
    
       // 获取父目录
       std::cout << "Parent path: " << p1.parent_path() << std::endl; // /home/user/documents
    
       // 获取根目录
       std::cout << "Root path: " << p1.root_path() << std::endl; // /
    
       // 获取扩展名
       std::cout << "Extension: " << p1.extension() << std::endl; // .txt
    
       // 判断路径是否是绝对路径
       std::cout << "Is absolute: " << p1.is_absolute() << std::endl; // true
       std::cout << "Is absolute: " << p2.is_absolute() << std::endl; // false
    
       return 0;
    }

    这个例子展示了如何创建 path 对象,如何拼接路径,以及如何获取路径的各个组成部分。 注意 / 操作符可以直接拼接 path 对象和字符串,非常方便。

  2. 文件和目录的创建、删除、重命名

    #include <iostream>
    #include <filesystem>
    
    namespace fs = std::filesystem;
    
    int main() {
       fs::path dir_path = "my_directory";
       fs::path file_path = dir_path / "my_file.txt";
    
       // 创建目录
       if (!fs::exists(dir_path)) {
           if (fs::create_directory(dir_path)) {
               std::cout << "Directory created successfully." << std::endl;
           } else {
               std::cerr << "Failed to create directory." << std::endl;
           }
       } else {
           std::cout << "Directory already exists." << std::endl;
       }
    
       // 创建文件 (创建空文件)
       std::ofstream outfile(file_path.string()); // 需要转换为 string
       outfile.close();
       std::cout << "File created successfully." << std::endl;
    
       // 重命名文件
       fs::path new_file_path = dir_path / "new_file.txt";
       fs::rename(file_path, new_file_path);
       std::cout << "File renamed successfully." << std::endl;
    
       // 删除文件
       fs::remove(new_file_path);
       std::cout << "File deleted successfully." << std::endl;
    
       // 删除目录 (只能删除空目录,否则会抛出异常)
       if (fs::is_empty(dir_path)) {
           fs::remove(dir_path);
           std::cout << "Directory deleted successfully." << std::endl;
       } else {
           std::cout << "Directory is not empty, cannot delete." << std::endl;
       }
    
       return 0;
    }

    这个例子展示了如何创建目录和文件,如何重命名文件,以及如何删除文件和目录。 注意:

    • 创建目录需要使用 fs::create_directory 函数。
    • 创建文件可以使用 std::ofstream,也可以使用 fs::create_symlink 创建符号链接。
    • 删除目录只能删除空目录,如果目录不为空,需要先删除目录下的所有文件和子目录。 可以使用 fs::remove_all 来递归删除目录及其内容,但要小心使用,避免误删重要文件。
    • fs::remove 如果删除失败会抛出异常,建议使用 try-catch 块来处理异常。
  3. 文件和目录的判断

    #include <iostream>
    #include <filesystem>
    
    namespace fs = std::filesystem;
    
    int main() {
       fs::path p = "test_file.txt";
    
       // 判断文件是否存在
       if (fs::exists(p)) {
           std::cout << "File exists." << std::endl;
       } else {
           std::cout << "File does not exist." << std::endl;
       }
    
       // 判断是否是文件
       if (fs::is_regular_file(p)) {
           std::cout << "It's a regular file." << std::endl;
       } else {
           std::cout << "It's not a regular file." << std::endl;
       }
    
       // 判断是否是目录
       if (fs::is_directory(p)) {
           std::cout << "It's a directory." << std::endl;
       } else {
           std::cout << "It's not a directory." << std::endl;
       }
    
       // 判断是否是符号链接
       if (fs::is_symlink(p)) {
           std::cout << "It's a symbolic link." << std::endl;
       } else {
           std::cout << "It's not a symbolic link." << std::endl;
       }
    
       return 0;
    }

    这个例子展示了如何判断文件或目录是否存在,以及如何判断文件类型。

  4. 文件大小、最后修改时间

    #include <iostream>
    #include <filesystem>
    #include <chrono>
    #include <ctime>
    
    namespace fs = std::filesystem;
    
    int main() {
       fs::path p = "example.txt";
    
       // 创建一个示例文件
       std::ofstream outfile(p.string());
       outfile << "Hello, world!";
       outfile.close();
    
       // 获取文件大小
       std::uintmax_t file_size = fs::file_size(p);
       std::cout << "File size: " << file_size << " bytes" << std::endl;
    
       // 获取最后修改时间
       auto last_write_time = fs::last_write_time(p);
       std::time_t cftime = std::chrono::system_clock::to_time_t(last_write_time);
       std::cout << "Last write time: " << std::ctime(&cftime) << std::endl;
    
       // 删除示例文件
       fs::remove(p);
    
       return 0;
    }

    这个例子展示了如何获取文件的大小和最后修改时间。 注意: last_write_time 返回的是 file_time_type 类型,需要转换为 std::time_t 才能格式化输出。

  5. 目录遍历

    #include <iostream>
    #include <filesystem>
    
    namespace fs = std::filesystem;
    
    int main() {
       fs::path dir_path = "."; // 当前目录
    
       // 遍历目录
       for (const auto& entry : fs::directory_iterator(dir_path)) {
           std::cout << entry.path() << std::endl;
    
           // 获取文件类型
           if (fs::is_regular_file(entry.path())) {
               std::cout << "  - Regular file" << std::endl;
           } else if (fs::is_directory(entry.path())) {
               std::cout << "  - Directory" << std::endl;
           }
       }
    
       // 递归遍历目录
       std::cout << "nRecursive directory iteration:" << std::endl;
       for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
           std::cout << entry.path() << std::endl;
       }
    
       return 0;
    }

    这个例子展示了如何遍历目录,可以使用 fs::directory_iterator 遍历目录下的直接子项,也可以使用 fs::recursive_directory_iterator 递归遍历目录下的所有子项。

四、文件状态 (file_status)

std::filesystem::file_status 类用于表示文件或目录的状态信息。可以使用 fs::status 函数获取文件状态。

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
    fs::path p = "example.txt";

    // 创建一个示例文件
    std::ofstream outfile(p.string());
    outfile << "Hello, world!";
    outfile.close();

    fs::file_status status = fs::status(p);

    // 判断文件类型
    if (fs::is_regular_file(status)) {
        std::cout << "It's a regular file." << std::endl;
    } else if (fs::is_directory(status)) {
        std::cout << "It's a directory." << std::endl;
    } else if (fs::is_symlink(status)) {
        std::cout << "It's a symbolic link." << std::endl;
    } else {
        std::cout << "It's something else." << std::endl;
    }

    // 获取权限信息 (平台相关)
    // 注意:权限信息的具体含义和表示方式在不同操作系统上可能不同。
    std::cout << "Permissions: " << static_cast<int>(status.permissions()) << std::endl;

    // 删除示例文件
    fs::remove(p);

    return 0;
}

file_status 可以用来判断文件类型,也可以获取权限信息。 注意:权限信息的具体含义和表示方式在不同操作系统上可能不同,所以在使用权限信息时需要小心处理。

五、空间信息 (space_info)

std::filesystem::space_info 类用于表示磁盘空间信息。可以使用 fs::space 函数获取磁盘空间信息。

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
    fs::path p = "/"; // 根目录

    fs::space_info space = fs::space(p);

    std::cout << "Capacity: " << space.capacity << " bytes" << std::endl;
    std::cout << "Free: " << space.free << " bytes" << std::endl;
    std::cout << "Available: " << space.available << " bytes" << std::endl;

    return 0;
}

space_info 可以用来获取磁盘的总容量、可用空间和空闲空间。

六、异常处理

std::filesystem 的很多函数在出错时会抛出异常,比如 fs::create_directory 在创建目录失败时会抛出异常,fs::remove 在删除文件失败时会抛出异常。

建议使用 try-catch 块来处理这些异常,避免程序崩溃。

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
    fs::path dir_path = "my_directory";

    try {
        fs::create_directory(dir_path);
        std::cout << "Directory created successfully." << std::endl;
    } catch (const fs::filesystem_error& e) {
        std::cerr << "Failed to create directory: " << e.what() << std::endl;
    }

    return 0;
}

七、线程安全

std::filesystem 的线程安全性取决于具体的实现和操作系统。一般来说,多个线程可以同时访问不同的文件,但是如果多个线程同时访问同一个文件,可能会导致数据竞争和未定义行为。

建议使用互斥锁等同步机制来保护共享的文件资源。

八、与其他库的结合

std::filesystem 可以和其他库结合使用,比如:

  • std::fstream 可以使用 std::fstream 来读写文件内容。
  • boost::filesystem 如果需要使用 C++17 之前的编译器,可以使用 boost::filesystem 库,它提供了类似的功能。

九、总结

std::filesystem 是 C++17 中一个非常强大的库,它提供了一套标准的、跨平台的、面向对象的文件系统操作接口,让咱们可以更方便地操作文件和目录。 掌握 std::filesystem,可以大大提高咱们的开发效率,写出更简洁、更易维护、更具可移植性的代码。

十、一些建议和注意事项

  • 错误处理: 务必进行错误处理,std::filesystem 操作可能会抛出异常。
  • 权限问题: 注意程序运行时的权限,确保程序有足够的权限进行文件系统操作。
  • 路径分隔符: 尽量使用 / 作为路径分隔符,std::filesystem 会自动将其转换为平台特定的分隔符。
  • 符号链接: 在处理符号链接时要小心,避免死循环或者安全问题。
  • 性能: 频繁的文件系统操作可能会影响性能,需要根据实际情况进行优化。
  • 跨平台: 虽然 std::filesystem 提供了跨平台的能力,但仍然有一些平台相关的差异,需要注意。

十一、一些常用函数表格整理

函数名 功能
fs::path 创建路径对象
path::operator/ 拼接路径
path::filename() 获取文件名
path::parent_path() 获取父目录
path::extension() 获取扩展名
fs::exists(path) 判断路径是否存在
fs::is_regular_file(path) 判断是否是普通文件
fs::is_directory(path) 判断是否是目录
fs::create_directory(path) 创建目录
fs::remove(path) 删除文件或空目录
fs::remove_all(path) 递归删除目录及其内容
fs::file_size(path) 获取文件大小
fs::last_write_time(path) 获取最后修改时间
fs::directory_iterator(path) 遍历目录下的直接子项
fs::recursive_directory_iterator(path) 递归遍历目录下的所有子项
fs::status(path) 获取文件状态
fs::space(path) 获取磁盘空间信息
fs::copy(from, to) 复制文件或者目录(可以选择复制选项,如覆盖、递归复制等)
fs::equivalent(path1, path2) 检查两个路径是否指向同一个文件或目录(通过比较设备ID和inode号等)
fs::canonical(path) 返回路径的绝对路径,解析所有符号链接和...等相对路径组件,并移除冗余的分隔符。
fs::relative(path, base) 返回一个从base路径到path路径的相对路径.
fs::current_path() 获取当前工作目录的路径
fs::absolute(path) 返回路径的绝对路径. 如果path已经是绝对路径,则直接返回path. 否则,将path与当前工作目录连接起来,形成绝对路径。

希望今天的分享能帮助大家更好地理解和使用 std::filesystem 库。 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注