22、filesystem库(C++17):路径操作、目录遍历、文件信息查询、实战:磁盘空间分析工具

说实话,C++程序员等一个标准化的文件系统库,等了太久了。在C++17之前,你要操作文件路径、遍历目录,要么用Boost.Filesystem,要么自己写平台相关的API——Windows上用GetFileAttributes,Linux上撸stat,代码写得又臭又长。我个人习惯是能不用平台API就不用,移植性太差了。

C++17引入的std::filesystem,说白了就是Boost.Filesystem的标准化版本。它把路径操作、目录遍历、文件信息查询这些日常高频需求,全部封装成了优雅的C++接口。今天我们就把它掰开揉碎,最后用这些知识写一个磁盘空间分析工具。

22.1 路径操作:别再用字符串拼接了

很多新手喜欢用字符串拼接来构造路径,比如string path = "/data/" + filename + ".txt";。嗯,这里要注意:不同操作系统的路径分隔符不一样,Windows用反斜杠,Linux用正斜杠。你手动拼接,换平台就崩。

std::filesystem::path就是来解决这个问题的。它自动处理分隔符,还提供了一系列成员函数来操作路径的各个部分。

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path p = "/home/user/docs/report.txt";

    std::cout << "根路径: " << p.root_path() << "\n";       // "/"
    std::cout << "父路径: " << p.parent_path() << "\n";     // "/home/user/docs"
    std::cout << "文件名: " << p.filename() << "\n";        // "report.txt"
    std::cout << "主干名: " << p.stem() << "\n";            // "report"
    std::cout << "扩展名: " << p.extension() << "\n";       // ".txt"

    // 拼接路径,自动处理分隔符
    fs::path newPath = p.parent_path() / "backup" / "report_2024.txt";
    std::cout << "新路径: " << newPath << "\n";
    // 输出: "/home/user/docs/backup/report_2024.txt"

    return 0;
}

我个人习惯用operator/=或者operator/来拼接路径,而不是用string+。你想想看,/操作符在路径语境下就是"追加子路径",语义非常清晰。

小技巧: 如果你需要把路径转成字符串传给旧接口,用p.string()。Windows下想获取原生格式(反斜杠),用p.native()

22.2 目录遍历:递归与非递归

遍历目录是文件系统操作里最常用的功能之一。比如你要统计一个项目里有多少个.cpp文件,或者要找出所有超过100MB的日志文件。C++17提供了两种迭代器:directory_iterator(非递归)和recursive_directory_iterator(递归)。

我在项目中遇到过一个问题:用recursive_directory_iterator遍历一个大目录树,结果遇到了符号链接循环,程序直接崩溃了。后来我加上了skip_permission_denied选项,才稳定下来。

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void list_files(const fs::path& dir) {
    // 非递归:只遍历当前目录
    std::cout << "=== 非递归遍历: " << dir << " ===\n";
    for (const auto& entry : fs::directory_iterator(dir)) {
        std::cout << (entry.is_directory() ? "[DIR] " : "[FILE] ")
                  << entry.path().filename() << "\n";
    }

    // 递归:遍历所有子目录
    std::cout << "=== 递归遍历: " << dir << " ===\n";
    for (const auto& entry : fs::recursive_directory_iterator(
             dir,
             fs::directory_options::skip_permission_denied)) {
        std::cout << std::string(entry.depth(), ' ')  // 缩进显示层级
                  << (entry.is_directory() ? "[DIR] " : "[FILE] ")
                  << entry.path().filename() << "\n";
    }
}
避坑指南: 我曾经在遍历网络挂载目录时,因为权限不足导致迭代器抛出filesystem_error异常。建议始终使用directory_options::skip_permission_denied,或者在外层捕获异常。

22.3 文件信息查询:不只是大小和修改时间

std::filesystem提供了file_status和一系列自由函数来查询文件信息。除了常见的文件大小、修改时间,还能查文件类型、权限、硬链接数等。

函数 返回值 说明
file_size(p) uintmax_t 文件大小(字节)
last_write_time(p) file_time_type 最后修改时间
status(p) file_status 文件类型和权限
is_directory(p) bool 是否是目录
is_regular_file(p) bool 是否是普通文件
hard_link_count(p) uintmax_t 硬链接数
void print_file_info(const fs::path& p) {
    try {
        auto ft = fs::last_write_time(p);
        auto ftime = decltype(ft)::clock::to_time_t(ft);
        std::cout << "文件: " << p.filename() << "\n"
                  << "  大小: " << fs::file_size(p) << " 字节\n"
                  << "  修改时间: " << std::ctime(&ftime)
                  << "  类型: " << (fs::is_directory(p) ? "目录" : "文件") << "\n";
    } catch (const fs::filesystem_error& e) {
        std::cerr << "查询失败: " << e.what() << "\n";
    }
}

22.4 实战:磁盘空间分析工具

好了,理论说完了,我们来写点真正有用的东西。磁盘空间分析工具——就是那种扫描目录,告诉你哪个文件夹占空间最大的工具。Windows上的TreeSize、Linux上的du命令,原理都差不多。

核心逻辑很简单:递归遍历目录,累加每个文件的大小,按目录分组统计。我写这个工具时,发现一个坑:file_size对目录调用会抛出异常,所以必须先判断是不是普通文件。

#include <filesystem>
#include <iostream>
#include <map>
#include <iomanip>

namespace fs = std::filesystem;

struct DirInfo {
    uintmax_t total_size = 0;
    uintmax_t file_count = 0;
    uintmax_t dir_count = 0;
};

// 递归扫描目录,返回该目录的总大小和信息
DirInfo scan_directory(const fs::path& dir, 
                       std::map<fs::path, DirInfo>& result,
                       int depth = 0) {
    DirInfo info;
    try {
        for (const auto& entry : fs::recursive_directory_iterator(
                 dir, fs::directory_options::skip_permission_denied)) {
            if (fs::is_regular_file(entry.path())) {
                uintmax_t size = fs::file_size(entry.path());
                info.total_size += size;
                info.file_count++;
            } else if (fs::is_directory(entry.path())) {
                info.dir_count++;
            }
        }
    } catch (const std::exception& e) {
        std::cerr << "扫描 " << dir << " 时出错: " << e.what() << "\n";
    }
    result[dir] = info;
    return info;
}

// 格式化显示大小
std::string format_size(uintmax_t bytes) {
    const char* units[] = {"B", "KB", "MB", "GB", "TB"};
    int unit_idx = 0;
    double size = static_cast<double>(bytes);
    while (size >= 1024.0 && unit_idx < 4) {
        size /= 1024.0;
        unit_idx++;
    }
    std::ostringstream oss;
    oss << std::fixed << std::setprecision(2) << size << " " << units[unit_idx];
    return oss.str();
}

int main(int argc, char* argv[]) {
    fs::path root = (argc > 1) ? argv[1] : fs::current_path();

    std::cout << "正在扫描: " << root << "\n";
    std::cout << "请稍候...\n\n";

    std::map<fs::path, DirInfo> result;
    DirInfo total = scan_directory(root, result);

    std::cout << "扫描完成!\n";
    std::cout << "总大小: " << format_size(total.total_size) << "\n";
    std::cout << "文件数: " << total.file_count << "\n";
    std::cout << "目录数: " << total.dir_count << "\n\n";

    // 按大小排序输出前10个目录
    std::cout << "占用空间最大的目录:\n";
    std::cout << std::left << std::setw(60) << "目录"
              << std::right << std::setw(15) << "大小"
              << std::setw(10) << "文件数" << "\n";
    std::cout << std::string(85, '-') << "\n";

    // 排序(这里简单起见,只输出根目录的信息)
    // 实际项目可以用vector排序
    for (const auto& [path, info] : result) {
        std::cout << std::left << std::setw(60) << path.string()
                  << std::right << std::setw(15) << format_size(info.total_size)
                  << std::setw(10) << info.file_count << "\n";
    }

    return 0;
}
核心要点:
  • recursive_directory_iterator递归遍历,加上skip_permission_denied避免权限错误
  • 先判断is_regular_file再调用file_size,否则会抛异常
  • map按目录路径存储统计结果,方便后续排序和展示
  • 格式化显示时,把字节数转成KB/MB/GB,人类可读

这个工具虽然简单,但已经能解决80%的磁盘空间分析需求了。你想想看,把它稍微扩展一下——加上排序、过滤、导出CSV——就是一个完整的实用工具。我在公司内部就用类似的脚本定期扫描服务器磁盘,自动发告警邮件。

filesystem 库知识体系 路径操作 目录遍历 文件信息查询 path 类:拼接、分解、转换 operator/ 自动处理分隔符 directory_iterator(非递归) recursive_directory_iterator ⚠️ 注意:skip_permission_denied file_size / last_write_time is_directory / is_regular_file ⚠️ 注意:目录调用 file_size 抛异常 实战:磁盘空间分析工具

以上就是filesystem库的核心内容。路径操作、目录遍历、文件信息查询,这三个技能组合起来,能解决大量日常文件处理需求。磁盘空间分析工具只是其中一个应用场景,你完全可以举一反三,写出文件同步工具、日志清理器、备份校验程序等等。

公众号:蓝海资料掘金营,微信deep3321