详解c++17 的 std::filesystem的核心功能与最佳实践

 

C++17引入的std::filesystem库为文件和目录操作提供了跨平台的标准解决方案,极大简化了文件系统操作的复杂性。本文详细介绍核心功能与使用技巧。

1. 基础概念

1.1 核心术语

// 关键概念
file         // 可读写的数据对象,包含内容与属性
directory    // 文件容器,包含其他文件的条目
path         // 标识文件位置的元素序列,可包含根名称、根目录与文件名序列

1.2 路径类型

// 三种主要路径类型
absolute_path    // 完整路径,无歧义标识文件位置(如"/home/user/file.txt")
canonical_path   // 不含符号链接、"."或".."元素的绝对路径 
relative_path    // 相对于某位置的路径(如"../docs/file.txt")

2. 核心功能类

2.1 path类

namespace fs = std::filesystem;

// 创建和操作路径
fs::path filepath{"dir/file.txt"};
std::cout << "文件名: " << filepath.filename() << '\n';     // "file.txt"
std::cout << "扩展名: " << filepath.extension() << '\n';    // ".txt"
std::cout << "父目录: " << filepath.parent_path() << '\n';  // "dir"

// 迭代路径组件
for(const auto& component : filepath) {
    std::cout << "<" << component.string() << "> ";  // <dir> <file.txt>
}

2.2 目录操作与迭代

// 目录迭代器:遍历单层目录内容
for(auto& entry : fs::directory_iterator{"/path/to/dir"}) {
    std::cout << entry.path() << '\n';
}

// 递归目录迭代器:深度优先遍历整个目录树
for(auto& entry : fs::recursive_directory_iterator{"/path/to/dir"}) {
    std::cout << "深度: " << entry.depth() << " - " << entry.path() << '\n';
}

// 目录条目:提供文件属性的缓存访问
fs::directory_entry entry{"/path/to/file.txt"};
if(entry.exists() && entry.is_regular_file()) {
    std::cout << "文件大小: " << entry.file_size() << '\n';
}

3. 常用操作函数

3.1 文件操作

// 文件检测
bool exists = fs::exists("file.txt");                      // 检查文件是否存在
bool is_dir = fs::is_directory("dir");                    // 是否目录
bool is_empty = fs::is_empty("file.txt");                 // 文件或目录是否为空

// 文件创建与修改
fs::create_directory("new_dir");                          // 创建单个目录
fs::create_directories("nested/dirs/structure");          // 创建嵌套目录结构
fs::copy("source.txt", "dest.txt");                       // 复制文件
fs::copy("src_dir", "dest_dir", 
         fs::copy_options::recursive);                    // 递归复制目录
fs::rename("old_name.txt", "new_name.txt");              // 重命名/移动文件
fs::remove("file.txt");                                   // 删除文件或空目录
uintmax_t count = fs::remove_all("dir");                  // 递归删除,返回删除的文件数

// 文件属性
uintmax_t size = fs::file_size("large_file.dat");        // 获取文件大小
fs::file_time_type time = fs::last_write_time("log.txt"); // 获取最后修改时间

3.2 路径处理

// 路径转换
fs::path abs_p = fs::absolute("../relative/path");        // 转换为绝对路径
fs::path canon_p = fs::canonical("/home/../user/./dir");  // 转换为规范路径 (/user/dir)
fs::path rel_p = fs::relative("C:/data/file.txt", "C:/"); // 获取相对路径 (data/file.txt)

// 路径比较
bool equiv = fs::equivalent("file.txt", "./file.txt");    // 检查是否指向同一文件

// 文件系统信息
fs::path temp = fs::temp_directory_path();                // 获取临时目录
fs::path curr = fs::current_path();                       // 获取当前工作目录

4. 实用示例

4.1 递归计算目录大小

uintmax_t get_directory_size(const fs::path& dir_path) {
    uintmax_t size = 0;
    
    // 检查路径是否存在且是目录
    if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
        return size;
    }
    
    // 递归遍历目录
    for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
        if (fs::is_regular_file(entry)) {
            // 累加文件大小
            size += fs::file_size(entry);
        }
    }
    
    return size;
}

// 使用示例
void print_dir_size() {
    fs::path dir = "/path/to/directory";
    
    try {
        uintmax_t size = get_directory_size(dir);
        std::cout << "目录大小: " << size << " 字节" << '\n';
        std::cout << "          " << (size / (1024.0 * 1024.0)) << " MB" << '\n';
    } catch (const fs::filesystem_error& e) {
        std::cerr << "错误: " << e.what() << '\n';
    }
}

4.2 简易文件备份工具

void backup_directory(const fs::path& source, const fs::path& backup_root) {
    // 创建带时间戳的备份目录
    auto now = std::chrono::system_clock::now();
    auto time_t_now = std::chrono::system_clock::to_time_t(now);
    
    std::stringstream ss;
    ss << std::put_time(std::localtime(&time_t_now), "%Y%m%d_%H%M%S");
    
    fs::path backup_dir = backup_root / (source.filename().string() + "_" + ss.str());
    
    try {
        // 创建备份目录
        fs::create_directories(backup_dir);
        
        // 复制内容
        fs::copy(source, backup_dir, 
                fs::copy_options::recursive | 
                fs::copy_options::update_existing);
        
        std::cout << "备份完成! 大小: " 
                  << (get_directory_size(backup_dir) / (1024.0 * 1024.0)) 
                  << " MB" << '\n';
    } catch (const fs::filesystem_error& e) {
        std::cerr << "备份错误: " << e.what() << '\n';
    }
}

5. 最佳实践

5.1 异常处理

try {
    fs::rename("source.txt", "destination.txt");
} catch(const fs::filesystem_error& e) {
    std::cerr << "错误: " << e.what() << '\n';
    std::cerr << "源路径: " << e.path1() << '\n';
    std::cerr << "目标路径: " << e.path2() << '\n';
}

5.2 性能优化技巧

// 低效方式: 每次调用status()都会查询文件系统
if (fs::is_regular_file(path) && fs::file_size(path) > 1024) { /* ... */ }

// 高效方式: 使用缓存的directory_entry
fs::directory_entry entry(path);
if (entry.is_regular_file() && entry.file_size() > 1024) { /* ... */ }

// 控制递归行为,避免不必要的遍历
fs::recursive_directory_iterator it(dir_path, fs::directory_options::skip_permission_denied);
for (auto it = fs::recursive_directory_iterator(dir_path); 
     it != fs::recursive_directory_iterator(); ++it) {
    if (should_skip_this_dir(*it)) {
        it.disable_recursion_pending(); // 不递归进入当前目录
    }
}

5.3 跨平台注意事项

// 不推荐:手动拼接路径
fs::path p = dir_path + "/" + filename; // 在Windows上可能有问题

// 推荐:使用路径连接操作符
fs::path p = dir_path / filename; // 在所有平台上都能正确工作

// 检查特定文件系统功能支持
try {
    fs::create_symlink("target_file", "link_to_file");
} catch (const fs::filesystem_error& e) {
    if (e.code() == std::errc::operation_not_supported) {
        std::cout << "当前文件系统不支持符号链接\n";
    }
}

6. 编译注意事项

# GNU实现(9.1之前)需要链接选项
g++ -std=c++17 main.cpp -lstdc++fs

# LLVM实现(9.0之前)需要链接选项
clang++ -std=c++17 main.cpp -lc++fs

# 较新版本通常不需要额外链接选项
g++ -std=c++17 main.cpp  # GNU 9.1+
clang++ -std=c++17 main.cpp  # LLVM 9.0+

总结

std::filesystem库极大简化了跨平台文件操作。掌握pathdirectory_entry和各种迭代器的用法,加上正确的异常处理,可以使文件操作代码更加健壮与可移植。

参考资料