本章讲解重要的文件系统类及函数,同时介绍可用于执行通用文件系统操作的有用编程技巧。讨论主题包括:
- 类 std::filesystem::path
- 类 std::filesystem::recursive_directory_iterator
- 类 std::filesystem::directory_entry
- 目录创建与删除函数
- 目录及文件拷贝函数
文件类型函数
STL 的文件系统类和函数定义在命名空间 std::filesystem 中。本文及源代码中对该命名空间的引用均使用前缀 fs::。时间库类来自命名空间 std::chrono,其引用则使用前缀 chrono::。本章讨论假定您已具备 Windows、Linux 或 macOS 目录、文件及路径名的基本知识。
文件系统路径类
命名空间 std::filesystem 中最实用的类之一是 fs::path。fs::path 类的实例包含一个表示文件系统路径的字符串路径名。这种表示仅涉及词法和语法层面。关于 fs:path 类需要理解的主要细节包括:
路径的组成元素包括根名称、根目录以及由分隔符划分的文件名序列。fs::path 中的根名称和根目录组件是可选的。
类 fs::path 支持多种路径名表示形式 ,包括绝对路径、相对路径和规范路径。
fs::path 对象并不一定对应存储设备上实际存在的目录或文件。
fs::path 对象包含的路径名不能保证在特定操作系统或其支持的任何文件系统中有效。
fs::path 路径名的最大长度由操作系统设定,操作系统也决定了有效的路径名字符。
示例函数 Ch17_01_ex1(),如代码清单 17-1-1 所示,引入了 fs::path 类。在其起始代码块中,Ch17_01_ex1() 使用 fs::path path1 = fs::current_path() 获取当前工作目录。执行 fs::current_path() 会返回一个原生操作系统格式的绝对路径(例如 X:\CppSTL\SourceCode\Chapter17 或 /home/homer/CppSTL/Code/Chapter17)。随后的 std::println() 语句通过 path1.string() 打印 path1 的路径名。1 代码清单 17-1-1 中的下一条语句 path1.append("test1.txt"),会将 \test1.txt 或 /test1.txt 追加到 path1。注意追加的文本包含操作系统特定的分隔符。
//-------------------------------------------------------------------------
// Ch17_01_ex.cpp
//-------------------------------------------------------------------------
#include <filesystem>
#include <fstream>
#include "Ch17_01.h"
#include "MF.h"
namespace fs = std::filesystem;
void Ch17_01_ex1()
{
// using fs::current_path
fs::path path1 = fs::current_path();
std::println("\npath1: {:s}", path1.string());
path1.append("test1.txt");
std::println("path1: {:s}", path1.string());
// using fs::temp_directory_path
fs::path path2 = fs::temp_directory_path();
std::println("\npath2: {:s}", path2.string());
path2 /= "test2.txt";
std::println("path2: {:s}", path2.string());
// using fs::current_path - bad path
fs::path path3 = fs::current_path();
std::println("\npath3: {:s}", path3.string());
path3 /= "Bad//Filename.txt";
std::println("path3: {:s}", path3.string());
std::ofstream ofs(path3);
std::println("\nofs.good(): {:s} (expecting false)", ofs.good());
}
Listing 17-1-1 中的下一个代码块利用 fs::path path2 = fs::temp_directory_path() 获取当前临时目录。fs::temp_directory_path() 返回的目录路径保证存在。在同一代码块中,执行 path2 /= "test2.txt" 会将特定于操作系统的目录分隔符附加到 path2(如果需要),后跟文本 test2.txt。
Ch17_01_ex1() 的最后一个代码块利用 fs::path path3 = fs::current_path() 和 path3 /= "Bad//Filename.txt" 来形成一个无效的路径名,以用于演示目的。回想一下,由 fs::path 对象表示的路径名不必有效。但是,使用 path3 创建文件将会失败,如在实例化 std::ofstream ofs(path3) 期间所示。
在 Listing 17-1-2 中,示例函数 Ch17_01_ex2() 使用 fs::path path1 = fs::current_path() / "test1.txt" 在当前工作目录中为文件 test1.txt 创建一个绝对路径名。随后的代码块演示了各种 fs::path 分解函数的使用,这些函数提取 fs::path 的不同组成部分。
void Ch17_01_ex2()
{
// create test path
fs::path path1 = fs::current_path() / "test1.txt";
std::println("path1: {:s}", path1.string());
// using fs::path decomposition functions
fs::path path1_root_name = path1.root_name();
fs::path path1_root_dir = path1.root_directory();
fs::path path1_root_path = path1.root_path();
fs::path path1_relative_path = path1.relative_path();
fs::path path1_parent_path = path1.parent_path();
fs::path path1_filename = path1.filename();
fs::path path1_stem = path1.stem();
fs::path path1_extension = path1.extension();
std::println("path1_root_name: {:s}", path1_root_name.string());
std::println("path1_root_dir: {:s}", path1_root_dir.string());
std::println("path1_root_path: {:s}", path1_root_path.string());
std::println("path1_relative_path: {:s}", path1_relative_path.string());
std::println("path1_parent_path: {:s}", path1_parent_path.string());
std::println("path1_filename: {:s}", path1_filename.string());
std::println("path1_stem: {:s}", path1_stem.string());
std::println("path1_extension: {:s}", path1_extension.string());
}
Ch17_01_ex2() 中使用的所有 fs::path 成员函数都返回 fs::path 类型的对象。表 17-1 和表 17-2 总结了在运行 Windows 和 Linux 的测试计算机上获得的结果。请注意,在这些表中,分解函数可以正确处理驱动器盘符(即根名称)说明符的存在与否。
路径组件 |
结果(windows) |
---|---|
绝对路径 |
X:\CppSTL\SourceCode\Chapter17\test1.txt |
root_name |
X: |
root_dir |
\ |
root_path |
X:\ |
relative_path |
CppSTL\SourceCode\Chapter17\test1.txt |
parent_path |
X:\CppSTL\SourceCode\Chapter17 |
filename |
test1.txt |
stem |
test1 |
extension |
.txt |
路径组件 |
结果(Linux) |
---|---|
绝对路径 |
/home/homer/SambaWin/CppSTL/SourceCode/Chapter17/test1.txt |
root_name |
|
root_dir |
/ |
root_path |
/ |
relative_path |
home/homer/SambaWin/CppSTL/SourceCode/Chapter17/test1.txt |
parent_path |
/home/homer/SambaWin/CppSTL/SourceCode/Chapter17 |
filename |
test1.txt |
stem |
test1 |
extension |
.txt |
下一个例子阐明了常用fs目录创建、存在和删除函数的使用。在清单17-1-3中,Ch17_01_ex3()的执行从fs::path sub_dir1 = fs::current_path() / "sub1"的初始化开始。随后的代码块利用rc = fs::exists(sub_dir1)来确定sub_dir1的存在。
void Ch17_01_ex3()
{
// initialize test subdirectory path
fs::path sub_dir1 = fs::current_path() / "sub1";
// using fs::exists
bool rc = fs::exists(sub_dir1);
std::println("\nfs::exists({:s})\nrc = {:s}", sub_dir1.string(), rc);
if (!rc)
{
// using fs::create_directory
rc = fs::create_directory(sub_dir1);
std::println("\nfs::create_directory({:s})\nrc = {:s}",
sub_dir1.string(), rc);
if (!rc)
return;
}
// write a test file to sub_dir1
fs::path fn1 = sub_dir1 / "TestA.txt";
rc = MF::create_test_file(fn1);
std::println("\nwrite_test_file({:s})\nrc = {:s}", fn1.string(), rc);
// using fs::exists
rc = fs::exists(fn1);
std::println("\nfs::exists({:s})\nrc = {:s}", fn1.string(), rc);
// using fs::remove to delete test file
rc = fs::remove(fn1);
std::println("\nfs::remove({:s})\nrc = {:s}", fn1.string(), rc);
// using fs::remove to delete test subdirectory (must be empty)
rc = fs::remove(sub_dir1);
std::println("\nfs::remove({:s})\nrc = {:s}", sub_dir1.string(), rc);
}
如果 sub_dir1 不存在,Ch17_01_ex3() 会执行 rc = fs::create_directory(sub_dir1) 来创建它。文件系统函数 fs::create_directory(sub_dir1) 如果创建了指定的目录,则返回 true;否则,返回 false。如果指定的目录已经存在,也会返回 false。这种情况将在后面更详细地讨论。
Ch17_01_ex3() 中的下一个代码块利用 fs::path fn1 = sub_dir1 / "TestA.txt" 和 MF::create_test_file(fn1)(参见清单 17-2-2-2)来创建测试文件 fn1。接下来是 fs:exists() 的另一个示例用法。Ch17_01_ex3() 的倒数第二个代码块执行 rc = fs::remove(fn1) 来删除测试文件 fn1。如果指定的文件被删除,则此函数的执行返回 true。Ch17_01_ex3() 的最后一个代码块利用 rc = fs::remove(sub_dir1) 来删除 sub_dir1。当使用 fs::remove() 删除目录时,该目录必须为空。否则,可能会抛出 fs::filesystem_error 异常。文件系统异常将在本章后面介绍。
清单 17-1-4 显示了示例 Ch17_01_ex4() 的源代码,该示例说明了如何创建和删除多层目录。开头的代码块 Ch17_01_ex4() 利用 fs::temp_directory_path() 和几个 fs::path 追加操作来初始化 fs::path sub_tree_top 和 fs::path sub_tree_bot。在随后的代码块中,执行 fs::create_directories(sub_tree_bot) 会创建目录 sub_tree_bot,包括任何所需的中间目录。对于当前示例,这将在 fs::temp_directory_path() 中创建目录子树 d1/d2/d3/d4。
void Ch17_01_ex4()
{
// create fs::paths
fs::path base_dir = fs::temp_directory_path();
fs::path sub_tree_top = base_dir / "d1";
fs::path sub_tree_bot = sub_tree_top / "d2/d3/d4";
// path sub_tree_bot exists?
bool rc = fs::exists(sub_tree_bot);
std::println("\nfs::exists({:s})\nrc = {:s}", sub_tree_bot.string(), rc);
if (!rc)
{
// using fs::create_directories to create sub_tree_bot
rc = fs::create_directories(sub_tree_bot);
std::println("\nfs::create_directories({:s})\nrc = {:s}",
sub_tree_bot.string(), rc);
if (!rc)
return;
}
// write test file to sub_tree_top
fs::path fn1 = sub_tree_top / "TestA.txt";
rc = MF::create_test_file(fn1);
std::println("\nfn1.generic_string(): {:s}", fn1.generic_string());
std::println("fn1.string(): {:s}", fn1.string());
std::println("rc: {:s}", rc);
// write test file to sub_tree_bot
fs::path fn2 = sub_tree_bot / "TestB.txt";
rc = MF::create_test_file(fn2);
std::println("\nfn2.generic_string(): {:s}", fn2.generic_string());
std::println("fn2.string(): {:s}", fn2.string());
std::println("rc: {:s}", rc);
// using fs::remove_all to delete sub_tree_top
auto num_deletes = fs::remove_all(sub_tree_top);
std::println("\nfs::remove_all({:s})\nnum_deletes = {:d}",
sub_tree_top.generic_string()