广告位 · 页头(后台-模块管理-新建模块后替换此区域)

C++如何用directory_iterator递归统计文件夹内所有文件的总行数?

统计代码量是评估项目规模、检查工作量或做代码审计时经常要做的事情。C++17标准库引入了<filesystem>头文件,提供了directory_iterator用于枚举目录项。不过它默认只遍历指定目录的直接子项,不会自动进入子目录。要想统计整个文件夹内所有文件的总行数,需要在遇到子目录时手动递归处理。本文会从基础遍历讲起,给出统计单文件行数的函数,再组合成完整可运行的递归统计程序,最后分析一些容易踩到的性能与异常处理问题。

C++如何用directory_iterator递归统计文件夹内所有文件的总行数?

递归遍历目录:从directory_iterator开始

directory_iterator的构造非常简单,传入一个std::filesystem::path对象即可开始迭代。每次解引用会得到一个directory_entry,通过它可以判断当前项是普通文件、目录还是符号链接。下面是一段只枚举当前层级所有目录项的示例:

#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
    fs::path root = fs::current_path();
    for (const auto& entry : fs::directory_iterator(root)) {
        std::cout << entry.path().string() << '\n';
    }
    return 0;
}

这段代码会打印传入目录下的所有文件、子目录以及符号链接名称,但不会进入子目录内部。要实现递归遍历,有两种思路。第一种是使用recursive_directory_iterator,它内部维护了一个目录栈,会自动深入每个子目录。第二种就是像本文标题所说的那样,只用directory_iterator自己写递归。手写递归的好处是可以在进入某个子目录前做更多判断,例如跳过隐藏目录、跳过第三方依赖目录,或者在递归深度过大时提前停止,避免无意义的遍历。

使用directory_iterator递归时,需要注意区分is_directory()和is_regular_file()。前者会跟随符号链接,也就是说如果某个目录项是指向目录的符号链接,is_directory()可能返回true。如果不加处理就递归进去,遇到循环符号链接时可能造成无限递归。更稳妥的做法是先用symlink_status()检查符号链接,再决定是否递归。对于普通文件,则调用is_regular_file()。权限不足时,构造directory_iterator或读取目录项会抛出filesystem_error异常,因此递归函数外层应当使用try/catch包裹。

统计单个文件的行数:注意最后一行和换行符

统计单个文件行数最直接的方式是用std::ifstream配合std::getline逐行读取。每成功读取一行,行数加一。这个逻辑看起来简单,但有一个容易误解的地方:如果文件最后一行没有换行符,getline仍然会把它当作一行返回,所以计数是正确的。例如文件内容为abc\n123,第一次读取到abc,第二次读取到123,最终行数为2,符合直觉。

#include <fstream>
#include <string>
#include <filesystem>
std::uintmax_t count_lines_in_file(const std::filesystem::path& file_path) {
    std::ifstream in(file_path, std::ios::in);
    if (!in.is_open()) {
        return 0;
    }
    std::uintmax_t lines = 0;
    std::string line;
    while (std::getline(in, line)) {
        ++lines;
    }
    return lines;
}

这个函数在大多数文本文件上表现正常,但要注意两个问题。第一个是二进制文件。如果目录里含有图片、可执行文件、压缩包等二进制内容,用文本模式打开后会读到大量乱码,getline仍然会按字节寻找换行符,统计出来的行数没有实际意义,而且可能非常慢。第二个是Windows平台上的文本模式会进行换行符转换,\r\n会被转换成\n,这对行数统计没有影响,但如果希望精确控制读取方式,可以显式使用std::ios::binary。为了避免二进制文件干扰,通常可以按扩展名过滤,只统计常见的源代码文件。

如果文件非常大,例如几百MB的日志文件,getline每次动态分配std::string并逐字符查找换行符,性能会明显下降。此时可以考虑使用std::ifstream的缓冲区读取,手动扫描换行符;或者使用内存映射文件的方式。但对于常规代码工程来说,逐个文件用getline已经足够,因为单个源码文件通常不会超过几十MB。

完整示例:递归统计目录下所有普通文件的行数

下面给出一个可以直接编译运行的完整程序。它从命令行参数读取根目录,递归遍历所有子目录,统计所有普通文件的行数,并在最后输出总行数和文件数量。程序内部统一捕获filesystem_error,单个文件打开失败时不会中断整个统计流程。

#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#include <cstdint>
#include <exception>
namespace fs = std::filesystem;
std::uintmax_t count_lines_in_file(const fs::path& file_path) {
    std::ifstream in(file_path, std::ios::in);
    if (!in.is_open()) {
        return 0;
    }
    std::uintmax_t lines = 0;
    std::string line;
    while (std::getline(in, line)) {
        ++lines;
    }
    return lines;
}
void count_lines_recursive(const fs::path& dir_path,
                           std::uintmax_t& total_lines,
                           std::uintmax_t& file_count) {
    try {
        for (const auto& entry : fs::directory_iterator(dir_path)) {
            const auto sym_status = entry.symlink_status();
            if (fs::is_symlink(sym_status)) {
                continue;
            }
            const auto file_status = entry.status();
            if (fs::is_regular_file(file_status)) {
                std::uintmax_t lines = count_lines_in_file(entry.path());
                total_lines += lines;
                ++file_count;
                std::cout << "文件: " << entry.path().string()
                          << " 行数: " << lines << '\n';
            } else if (fs::is_directory(file_status)) {
                count_lines_recursive(entry.path(), total_lines, file_count);
            }
        }
    } catch (const fs::filesystem_error& e) {
        std::cerr << "目录访问失败: " << e.what() << '\n';
    }
}
int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "用法: " << argv[0] << " <根目录>\n";
        return 1;
    }
    fs::path root_path = fs::path(argv[1]);
    if (!fs::exists(root_path) || !fs::is_directory(root_path)) {
        std::cerr << "目录不存在或不可访问: " << root_path.string() << '\n';
        return 1;
    }
    std::uintmax_t total_lines = 0;
    std::uintmax_t file_count = 0;
    count_lines_recursive(root_path, total_lines, file_count);
    std::cout << "统计完成\n";
    std::cout << "文件总数: " << file_count << '\n';
    std::cout << "总行数: " << total_lines << '\n';
    return 0;
}

程序的主体逻辑围绕递归函数count_lines_recursive展开。每次进入目录都会创建directory_iterator,迭代其中每一项。先通过symlink_status排除符号链接,避免重复遍历或递归循环;再根据status判断是普通文件还是目录。普通文件交给行数统计函数,目录则递归调用自身。这样实现结构清晰,过滤规则也容易扩展。

如果需要只统计C++源码文件,可以在判断普通文件之后增加扩展名检查。例如只处理.cpp、.h、.hpp文件,可以写一个简单的辅助函数,使用entry.path().extension()判断。对于大型工程,还应该把build、.git、node_modules等目录排除掉,否则统计结果会包含大量无关文件。

性能优化与常见陷阱

递归深度是手写递归需要注意的第一个问题。在极端情况下,目录层级可能超过系统栈容量,导致栈溢出。对于普通项目来说,几十层目录深度不会触发问题;但如果要处理不可信的目录结构,最好改用显式栈来模拟递归,或者直接使用recursive_directory_iterator,它内部使用迭代器状态而不是函数调用栈,自然避免了栈深度问题。在大多数代码统计场景下,递归深度完全可控,手写递归仍然是最灵活的选择。

第二个常见问题是权限错误。遍历系统目录或共享网络目录时,某些子目录可能没有读取权限。如果directory_iterator构造时抛异常,整个递归会中断。上面的完整示例在外层捕获了filesystem_error,让它跳过无法访问的目录继续统计其他内容。这样做能提升健壮性,但也要注意:异常捕获会掩盖一些真实的路径错误,调试时可以暂时去掉try/catch,方便定位问题。

第三个问题是行数定义。不同工具对行数的定义可能不同,有的只统计非空行,有的统计有效代码行,排除注释和空行。std::getline的方式把空行也算一行,这也是最简单的统计口径。如果要做更精细的统计,例如排除注释行,就需要加入简单的解析逻辑。对于C/C++代码,可以跳过只包含空白字符的行;对于Python代码,还要考虑#注释、三引号块注释等。这些扩展可以在count_lines_in_file函数内部继续完善。

最后还要注意文件编码。Windows平台下,中文文件名可能使用GBK编码,而std::filesystem::path在C++17中默认使用本地窄字符编码。如果命令行参数传入的是UTF-8编码路径,可能无法正确打开文件。可以使用std::filesystem::u8path构造路径,或者在程序启动时设置合适的locale。C++20对char8_t和u8string的支持更加规范,遇到乱码路径时可以优先考虑升级到C++20标准。总体来说,只要目录结构清晰、权限正常,基于directory_iterator手写递归统计行数是一个稳定且容易维护的方案。

广告位 · 文章底部(后台-模块管理替换此区域)