C++ `std::filesystem` 深度:复杂文件系统操作与错误处理

好的,让我们来一场关于 C++ std::filesystem 的深度探险,目标是搞定那些复杂的文件系统操作,并优雅地处理各种错误,让你的代码在文件系统的世界里也能横着走!

开场白:文件系统,程序猿的后花园

各位程序猿朋友们,大家好!欢迎来到本次“C++ std::filesystem 深度游”讲座。文件系统,对于我们来说,就像后花园一样,我们经常在里面创建、修改、删除各种文件和目录。但是,这个后花园也充满了陷阱,一不小心就会踩到雷,导致程序崩溃或者数据丢失。所以,掌握好 std::filesystem 这个工具,才能让我们在这个后花园里自由驰骋,种出我们想要的代码之花。

第一站:std::filesystem 的基本概念和常用操作

首先,我们要明确 std::filesystem 是什么?它就是 C++17 引入的一个标准库,专门用来处理文件系统相关的操作。有了它,我们就可以用 C++ 代码来创建目录、删除文件、遍历目录等等,而不用再依赖平台特定的 API。

1. path:文件系统的导航仪

std::filesystem::path 是整个库的核心,它代表文件系统中的一个路径。你可以把它想象成一个导航仪,告诉程序要去哪里。

#include <iostream>
#include <filesystem>

int main() {
  std::filesystem::path my_path = "/home/user/documents/my_file.txt";

  std::cout << "全路径: " << my_path << std::endl;
  std::cout << "文件名: " << my_path.filename() << std::endl;
  std::cout << "父目录: " << my_path.parent_path() << std::endl;
  std::cout << "根目录: " << my_path.root_path() << std::endl;
  std::cout << "是否是绝对路径: " << my_path.is_absolute() << std::endl;

  return 0;
}

这段代码展示了 path 的基本用法,你可以用它来获取文件名、父目录、根目录等等,还可以判断路径是绝对路径还是相对路径。

2. 文件和目录的基本操作

有了 path 这个导航仪,我们就可以开始进行文件和目录的基本操作了。

  • 创建目录:create_directorycreate_directories

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path dir_path = "my_new_directory";
    
      try {
        if (!std::filesystem::exists(dir_path)) {
          std::filesystem::create_directory(dir_path); // 创建单个目录
          std::cout << "目录 " << dir_path << " 创建成功!" << std::endl;
        } else {
          std::cout << "目录 " << dir_path << " 已经存在!" << std::endl;
        }
    
        std::filesystem::path nested_dir_path = "nested/directory/structure";
        std::filesystem::create_directories(nested_dir_path); // 创建多级目录
        std::cout << "多级目录 " << nested_dir_path << " 创建成功!" << std::endl;
    
      } catch (const std::exception& e) {
        std::cerr << "创建目录失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    create_directory 用于创建单个目录,如果目录已经存在,会抛出异常。create_directories 用于创建多级目录,如果中间的目录不存在,会自动创建。

  • 删除文件和目录:removeremove_all

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path file_path = "my_file.txt";
      std::filesystem::path dir_path = "my_new_directory";
    
      try {
        // 创建一个空文件
        std::ofstream outfile(file_path);
        outfile.close();
    
        if (std::filesystem::exists(file_path)) {
          std::filesystem::remove(file_path); // 删除文件
          std::cout << "文件 " << file_path << " 删除成功!" << std::endl;
        } else {
          std::cout << "文件 " << file_path << " 不存在!" << std::endl;
        }
    
        if (std::filesystem::exists(dir_path)) {
          std::filesystem::remove_all(dir_path); // 删除目录及其内容
          std::cout << "目录 " << dir_path << " 及其内容 删除成功!" << std::endl;
        } else {
          std::cout << "目录 " << dir_path << " 不存在!" << std::endl;
        }
      } catch (const std::exception& e) {
        std::cerr << "删除文件/目录失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    remove 用于删除文件或空目录,如果目录不为空,会删除失败。remove_all 用于删除目录及其所有内容,包括子目录和文件,要小心使用!

  • 复制文件:copy

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path source_file = "source.txt";
      std::filesystem::path destination_file = "destination.txt";
    
      try {
        // 创建一个源文件
        std::ofstream outfile(source_file);
        outfile << "This is the source file.";
        outfile.close();
    
        std::filesystem::copy(source_file, destination_file, std::filesystem::copy_options::overwrite_existing);
        std::cout << "文件 " << source_file << " 复制到 " << destination_file << " 成功!" << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "复制文件失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    copy 用于复制文件,可以指定复制选项,比如 overwrite_existing 表示如果目标文件已经存在,则覆盖它。

  • 重命名/移动文件或目录:rename

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path old_name = "old_file.txt";
      std::filesystem::path new_name = "new_file.txt";
    
      try {
        // 创建一个源文件
        std::ofstream outfile(old_name);
        outfile << "This is the old file.";
        outfile.close();
    
        std::filesystem::rename(old_name, new_name);
        std::cout << "文件 " << old_name << " 重命名为 " << new_name << " 成功!" << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "重命名文件失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    rename 用于重命名文件或目录,也可以用来移动文件或目录到新的位置。

第二站:文件属性和状态查询

除了基本的文件操作,我们还需要了解文件的属性和状态,比如文件大小、创建时间、修改时间等等。

  • 文件大小:file_size

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path file_path = "my_file.txt";
    
      try {
        // 创建一个源文件
        std::ofstream outfile(file_path);
        outfile << "This is the file content.";
        outfile.close();
    
        std::uintmax_t file_size = std::filesystem::file_size(file_path);
        std::cout << "文件 " << file_path << " 的大小是: " << file_size << " 字节" << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "获取文件大小失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    file_size 函数可以获取文件的大小,返回的是字节数。

  • 文件类型:is_regular_fileis_directoryis_symlink

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path file_path = "my_file.txt";
      std::filesystem::path dir_path = "my_directory";
    
      try {
        // 创建一个源文件
        std::ofstream outfile(file_path);
        outfile << "This is the file content.";
        outfile.close();
    
        std::filesystem::create_directory(dir_path);
    
        if (std::filesystem::is_regular_file(file_path)) {
          std::cout << file_path << " 是一个普通文件" << std::endl;
        }
    
        if (std::filesystem::is_directory(dir_path)) {
          std::cout << dir_path << " 是一个目录" << std::endl;
        }
    
        if (std::filesystem::exists("nonexistent_file.txt")) {
            std::cout << "This should not be printed." << std::endl;
        } else {
            std::cout << "nonexistent_file.txt does not exist." << std::endl;
        }
    
      } catch (const std::exception& e) {
        std::cerr << "获取文件类型失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    这些函数可以判断文件是否是普通文件、目录、符号链接等等。

  • 最后修改时间:last_write_time

    #include <iostream>
    #include <filesystem>
    #include <chrono>
    
    int main() {
      std::filesystem::path file_path = "my_file.txt";
    
      try {
        // 创建一个源文件
        std::ofstream outfile(file_path);
        outfile << "This is the file content.";
        outfile.close();
    
        auto last_write_time = std::filesystem::last_write_time(file_path);
        std::time_t tt = std::chrono::system_clock::to_time_t(last_write_time);
        std::cout << "文件 " << file_path << " 的最后修改时间是: " << std::ctime(&tt) << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "获取最后修改时间失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    last_write_time 函数可以获取文件的最后修改时间,返回的是一个 std::filesystem::file_time_type 对象,需要转换成 std::time_t 才能显示。

第三站:目录遍历

目录遍历是文件系统操作中非常常见的需求,比如查找特定类型的文件、统计文件数量等等。std::filesystem 提供了 directory_iteratorrecursive_directory_iterator 来实现目录遍历。

  • directory_iterator:遍历当前目录

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path dir_path = "."; // 当前目录
    
      try {
        std::cout << "当前目录下的文件和目录:" << std::endl;
        for (const auto& entry : std::filesystem::directory_iterator(dir_path)) {
          std::cout << entry.path() << std::endl;
        }
      } catch (const std::exception& e) {
        std::cerr << "遍历目录失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    directory_iterator 只能遍历当前目录下的文件和目录,不会进入子目录。

  • recursive_directory_iterator:递归遍历所有子目录

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path dir_path = "."; // 当前目录
    
      try {
        std::cout << "当前目录及其所有子目录下的文件和目录:" << std::endl;
        for (const auto& entry : std::filesystem::recursive_directory_iterator(dir_path)) {
          std::cout << entry.path() << std::endl;
        }
      } catch (const std::exception& e) {
        std::cerr << "遍历目录失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    recursive_directory_iterator 会递归遍历所有子目录,直到所有文件和目录都被访问到。

    使用技巧:depth()recursion_pending()

    在递归遍历目录时,我们可能需要知道当前的深度,或者控制是否进入子目录。recursive_directory_iterator 提供了 depth()recursion_pending() 方法来实现这些功能。

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path dir_path = "."; // 当前目录
    
      try {
        for (auto it = std::filesystem::recursive_directory_iterator(dir_path); it != std::filesystem::recursive_directory_iterator(); ++it) {
          std::cout << it->path() << " (深度: " << it.depth() << ")" << std::endl;
    
          // 如果是目录,并且深度大于 1,则不进入该目录
          if (std::filesystem::is_directory(it->path()) && it.depth() > 1) {
            it.disable_recursion_pending();
            std::cout << "  跳过该目录" << std::endl;
          }
        }
      } catch (const std::exception& e) {
        std::cerr << "遍历目录失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    depth() 返回当前遍历的深度,disable_recursion_pending() 可以阻止进入当前目录的子目录。

第四站:符号链接

符号链接(Symbolic Link),也称为软链接,是指向另一个文件或目录的指针。std::filesystem 提供了创建、读取和判断符号链接的功能。

  • 创建符号链接:create_symlinkcreate_directory_symlink

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path target_file = "target.txt";
      std::filesystem::path symlink_file = "symlink.txt";
      std::filesystem::path target_dir = "target_directory";
      std::filesystem::path symlink_dir = "symlink_directory";
    
      try {
        // 创建目标文件
        std::ofstream outfile(target_file);
        outfile << "This is the target file.";
        outfile.close();
    
        // 创建目标目录
        std::filesystem::create_directory(target_dir);
    
        std::filesystem::create_symlink(target_file, symlink_file); // 创建文件符号链接
        std::cout << "文件符号链接 " << symlink_file << " 创建成功!" << std::endl;
    
        std::filesystem::create_directory_symlink(target_dir, symlink_dir); // 创建目录符号链接
        std::cout << "目录符号链接 " << symlink_dir << " 创建成功!" << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "创建符号链接失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    create_symlink 用于创建文件符号链接,create_directory_symlink 用于创建目录符号链接。

  • 读取符号链接的目标:read_symlink

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path symlink_file = "symlink.txt";
    
      try {
        std::filesystem::path target_path = std::filesystem::read_symlink(symlink_file);
        std::cout << "符号链接 " << symlink_file << " 指向: " << target_path << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "读取符号链接失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    read_symlink 函数可以读取符号链接指向的目标路径。

  • 判断是否是符号链接:is_symlink

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path symlink_file = "symlink.txt";
    
      try {
        if (std::filesystem::is_symlink(symlink_file)) {
          std::cout << symlink_file << " 是一个符号链接" << std::endl;
        } else {
          std::cout << symlink_file << " 不是一个符号链接" << std::endl;
        }
      } catch (const std::exception& e) {
        std::cerr << "判断符号链接失败: " << e.what() << std::endl;
      }
    
      return 0;
    }

    is_symlink 函数可以判断一个路径是否是符号链接。

第五站:错误处理

文件系统操作经常会遇到各种错误,比如文件不存在、权限不足等等。std::filesystem 提供了异常处理机制来处理这些错误。

  • 使用 try-catch 块捕获异常

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path file_path = "nonexistent_file.txt";
    
      try {
        std::uintmax_t file_size = std::filesystem::file_size(file_path);
        std::cout << "文件大小: " << file_size << std::endl;
      } catch (const std::filesystem::filesystem_error& e) {
        std::cerr << "文件系统错误: " << e.what() << std::endl;
        std::cerr << "错误码: " << e.code() << std::endl;
      } catch (const std::exception& e) {
        std::cerr << "其他错误: " << e.what() << std::endl;
      }
    
      return 0;
    }

    使用 try-catch 块可以捕获 std::filesystem::filesystem_error 类型的异常,这个异常包含了错误信息和错误码。

  • error_code:更细粒度的错误处理

    除了异常处理,std::filesystem 还提供了 error_code 来进行更细粒度的错误处理。

    #include <iostream>
    #include <filesystem>
    
    int main() {
      std::filesystem::path file_path = "nonexistent_file.txt";
      std::error_code ec;
    
      std::uintmax_t file_size = std::filesystem::file_size(file_path, ec);
    
      if (ec) {
        std::cerr << "文件系统错误: " << ec.message() << std::endl;
        std::cerr << "错误码: " << ec.value() << std::endl;
      } else {
        std::cout << "文件大小: " << file_size << std::endl;
      }
    
      return 0;
    }

    error_code 对象作为参数传递给文件系统函数,如果发生错误,错误信息会保存在 error_code 对象中,而函数不会抛出异常。

总结:std::filesystem 的最佳实践

  • 始终进行错误处理: 无论是使用异常还是 error_code,都要确保对文件系统操作可能出现的错误进行处理,避免程序崩溃或者数据丢失。
  • 小心使用 remove_all 这个函数会删除目录及其所有内容,要确保你的代码逻辑正确,避免误删重要数据。
  • 注意权限问题: 在进行文件系统操作时,要确保程序有足够的权限,否则会抛出异常或者返回错误码。
  • 考虑平台兼容性: 虽然 std::filesystem 是标准库,但在不同的操作系统上,文件系统的行为可能略有不同,需要进行适当的兼容性处理。
  • 使用 RAII 风格: 使用 RAII (Resource Acquisition Is Initialization) 风格管理文件资源,确保资源在使用完毕后能够被正确释放,避免资源泄漏。 例如,使用 std::fstream 对象,在对象析构时会自动关闭文件。

表格:std::filesystem 常用函数总结

函数名 功能
create_directory 创建单个目录
create_directories 创建多级目录
remove 删除文件或空目录
remove_all 删除目录及其所有内容
copy 复制文件
rename 重命名/移动文件或目录
file_size 获取文件大小
is_regular_file 判断是否是普通文件
is_directory 判断是否是目录
is_symlink 判断是否是符号链接
last_write_time 获取最后修改时间
directory_iterator 遍历当前目录
recursive_directory_iterator 递归遍历所有子目录
create_symlink 创建文件符号链接
create_directory_symlink 创建目录符号链接
read_symlink 读取符号链接的目标
exists 检查路径是否存在

结束语:文件系统,尽在掌握

今天的 std::filesystem 深度游就到这里了。希望通过这次讲座,大家能够对 std::filesystem 有更深入的了解,掌握文件系统操作的各种技巧,并在实际开发中灵活运用。记住,文件系统是程序猿的后花园,只要我们用心呵护,就能种出我们想要的代码之花!

谢谢大家!

发表回复

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