如何在 C++17 中使用 std::filesystem 进行跨平台文件操作

C++17 标准库新增了 std::filesystem 模块,它为文件系统相关操作提供了统一、跨平台的接口。相比传统的 POSIX 或 Windows API,std::filesystem 更加简洁、类型安全,并且能在编译时捕获错误。下面从安装、核心概念、常用操作、异常处理以及性能注意事项等方面,系统介绍如何使用 std::filesystem。

1. 环境准备

  • 编译器:g++ 8.1+、clang++ 6.0+、MSVC 2017+ 支持 std::filesystem。
  • 标准:在编译时加上 -std=c++17(或更高)并链接 -lstdc++fs(部分实现需要):
    g++ -std=c++17 -lstdc++fs main.cpp -o main

2. 基本命名空间与类型

#include <filesystem>
namespace fs = std::filesystem;

核心类型:

  • fs::path:路径对象,支持字符串、字符类型等构造。
  • fs::directory_entry:目录项,用于遍历目录。
  • fs::directory_iterator / recursive_directory_iterator:迭代器。
  • fs::file_status / fs::space_info:文件状态与磁盘空间信息。

3. 常用操作

功能 示例代码 说明
判断路径存在 if (fs::exists(p)) { … } 检查文件或目录是否存在
创建目录 fs::create_directories(p); 递归创建多级目录
删除文件/目录 fs::remove(p); / fs::remove_all(p); 删除单个或递归删除
重命名 fs::rename(old_p, new_p); 更改文件或目录名称
读取文件大小 auto sz = fs::file_size(p); 以字节为单位返回
复制文件 fs::copy(source, dest, fs::copy_options::overwrite_existing); 可设置复制选项
遍历目录 for (const auto &entry : fs::directory_iterator(dir)) { … } 非递归
递归遍历 for (const auto &entry : fs::recursive_directory_iterator(dir)) { … } 递归

4. 路径操作

fs::path p = "/usr/local/bin";
std::cout << "文件名: " << p.filename() << '\n';          // bin
std::cout << "父目录: " << p.parent_path() << '\n';      // /usr/local
std::cout << "是否为绝对路径: " << p.is_absolute() << '\n';

使用 operator/ 拼接路径更加直观:

fs::path log_dir = "/var/log";
fs::path log_file = log_dir / "app.log";

5. 处理符号链接

fs::path link = "/tmp/link";
if (fs::is_symlink(link)) {
    fs::path target = fs::read_symlink(link);   // 读取指向的目标
}

6. 异常处理

std::filesystem 的大多数操作会抛出 std::filesystem::filesystem_error。你可以通过捕获该异常来获取错误码与路径信息。

try {
    fs::create_directory("/root/forbidden");
} catch (const fs::filesystem_error &e) {
    std::cerr << "Error: " << e.what() << '\n';
    std::cerr << "Error code: " << e.code() << '\n';
}

如果你不想抛出异常,可以使用带 std::error_code& 参数的函数。例如:

std::error_code ec;
fs::create_directory("/root/forbidden", ec);
if (ec) {
    std::cout << "创建失败: " << ec.message() << '\n';
}

7. 性能与线程安全

  • 批量操作:尽量一次性完成相关操作,避免频繁系统调用。
  • 缓存:若在多线程环境下频繁查询同一目录,可自行缓存 directory_entry 对象。
  • 递归删除fs::remove_all 在大目录下可能会出现性能瓶颈;可考虑使用 boost::filesystem::recursive_directory_iterator 的手动实现进行分批删除。

8. 示例:复制一个目录树

下面给出一个完整的例子,将源目录复制到目标目录,并保留符号链接。

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

void copy_dir(const fs::path &src, const fs::path &dst) {
    if (!fs::exists(src) || !fs::is_directory(src)) {
        throw std::runtime_error("源目录不存在或不是目录");
    }

    fs::create_directories(dst);

    for (const auto &entry : fs::recursive_directory_iterator(src)) {
        const auto &path_in_src = entry.path();
        auto relative_path = fs::relative(path_in_src, src);
        auto path_in_dst = dst / relative_path;

        try {
            if (entry.is_symlink()) {
                auto target = fs::read_symlink(path_in_src);
                fs::create_symlink(target, path_in_dst);
            } else if (entry.is_directory()) {
                fs::create_directory(path_in_dst);
            } else if (entry.is_regular_file()) {
                fs::copy_file(path_in_src, path_in_dst, fs::copy_options::overwrite_existing);
            }
        } catch (const fs::filesystem_error &e) {
            std::cerr << "复制失败: " << e.what() << '\n';
        }
    }
}

int main() {
    try {
        copy_dir("/home/user/project", "/home/user/backup/project");
        std::cout << "复制完成\n";
    } catch (const std::exception &e) {
        std::cerr << "错误: " << e.what() << '\n';
        return 1;
    }
    return 0;
}

9. 小结

  • std::filesystem 让文件系统操作更安全、可读且跨平台。
  • 通过异常或错误码两种方式灵活处理错误。
  • 结合 fs::path 的强大路径处理能力,减少手动字符串拼接错误。
  • 在性能要求高的场景,注意批量操作与缓存。

掌握上述基本用法后,你可以在 C++17 或更高版本的项目中无缝使用 std::filesystem,既提升代码质量,又兼顾跨平台的兼容性。

发表评论