文件系统 - 文件与目录的 CRUD

文件系统 - 文件与目录的 CRUD
苏丙榅在 C++17 引入的 std::filesystem 库中,文件和目录的增删改查(CRUD)操作变得极其简单且跨平台。不再需要依赖底层的 POSIX API(如 open, stat, unlink)或 Windows API(CreateFile, DeleteFile),而是使用统一的 C++ 接口。
1. 创建与复制
在 C++17 std::filesystem 中,文件的增操作主要涉及文件/目录的创建、复制以及内容的写入。由于 C++ 文件系统库主要关注路径和元数据的管理,创建文件内容通常仍然依赖传统的fstream库,而创建目录结构则是 filesystem 的强项。
1.1 创建
创建操作主要分为:创建空目录、创建目录树(多级目录)、以及创建空文件。
创建目录
1
bool create_directory(const std::filesystem::path& p);
功能:尝试创建一个由路径
p指定的目录,如果父目录不存在,则会抛出异常。返回值:
- 如果目录创建成功,或者目录已经存在,返回
true。 - 如果创建失败(例如父目录不存在,或者路径指向一个文件而非目录),返回
false。
- 如果目录创建成功,或者目录已经存在,返回
特点:它只能创建单级目录。父目录必须已经存在,否则失败。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace fs = std::filesystem;
int main()
{
// 创建单级目录 "data"
bool created = fs::create_directory("data");
if (created)
{
// 如果目录不存在且创建成功,created 为 true
}
// 再次尝试创建,返回 true (表示目录已存在)
bool exists = fs::create_directory("data");
std::cout << "exists : " << exists << std::endl; // false
return 0;
}创建多级目录
1
bool create_directories(const std::filesystem::path& p);
功能:创建路径
p指定的目录,如果路径中包含不存在的父目录,它会递归地自动创建所有缺失的父目录。返回值:
- 如果目录被创建(包括创建的父目录),返回
true。 - 如果目录已经存在,返回
false。
- 如果目录被创建(包括创建的父目录),返回
注意:如果路径
p已经是一个存在的目录,它什么也不做并返回false。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace fs = std::filesystem;
int main()
{
// 创建单级目录 "data"
bool created = fs::create_directories("a/b/c");
if (created)
{
// 如果目录不存在且创建成功,created 为 true
}
// 再次尝试创建,返回 true (表示目录已存在)
bool exists = fs::create_directories("a/b/c");
std::cout << "exists : " << exists << std::endl;
return 0;
}创建空文件
std::filesystem没有直接提供create_file函数。标准做法是利用传统的std::ofstream来创建一个空文件。示例代码如下:1
2
3
4
5
6// 如果文件不存在,这会创建它;如果存在,这会截断它(清空内容)
std::ofstream("config.ini").close();
// 或者使用 C++17 的 fopen 风格
std::FILE* fp = std::fopen("empty.txt", "w");
if (fp) std::fclose(fp);
1.2 复制
C++17 提供了非常灵活的复制功能,支持递归复制目录、控制覆盖行为以及仅复制元数据。
拷贝文件
1
2
3void copy(const std::filesystem::path& from, const std::filesystem::path& to);
void copy(const std::filesystem::path& from, const std::filesystem::path& to, std::error_code& ec) noexcept;
void copy(const std::filesystem::path& from, const std::filesystem::path& to, copy_options options);功能:将
from复制到to。参数:
- 如果
from是文件,则复制内容。 - 如果
from是目录,默认情况下不会递归复制内容(见下文copy_options)。 copy_options枚举:none:默认行为。skip_existing:如果目标已存在,跳过复制(不覆盖)。overwrite_existing:如果目标已存在,覆盖它。update_existing:如果目标已存在,仅当源文件比目标文件新时才覆盖。recursive:关键选项。递归复制子目录及其内容。copy_symlinks:复制符号链接本身,而不是链接指向的文件。directories_only:仅复制目录,不复制文件。
- 如果
异常:如果目标已存在,默认会抛出异常(除非使用特定的
copy_options或者错误由error_code捕获)。示例:
1
2
3
4
5// 将 file.txt 复制为 file_backup.txt,如果文件已存在则覆盖
fs::copy("file.txt", "file_backup.txt", fs::copy_options::overwrite_existing);
// 递归复制整个 "project" 文件夹到 "project_backup"
fs::copy("project", "project_backup", fs::copy_options::recursive);
复制文件元数据
如果我们只想复制文件的权限、修改时间等属性,而不想复制文件内容(或者内容已经复制完了,只想同步属性)可以使用
copy_file函数:1
void copy_file(const std::filesystem::path& from, const std::filesystem::path& to, copy_options options = copy_options::none);
std::filesystem对复制文件内容和复制文件属性在概念上是区分的,但copy_file函数通常两者都做。如果你想只复制属性,通常需要手动读取并写入时间戳和权限,或者使用copy加上skip_existing(但这不一定适用于属性同步,最标准的方式是用permissions和last_write_time函数单独设置)。下面是一个展示如何创建目录树、配置文件并将其备份的完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
namespace fs = std::filesystem;
void initialize_project(const std::string& project_name)
{
fs::path root = project_name;
fs::path src = root / "src";
fs::path config_file = root / "config.json";
try
{
// 1. 创建多级目录结构
if (fs::create_directories(src))
{
std::cout << "Created directories: " << src << std::endl;
}
// 2. 创建空文件 (并写入初始内容)
std::ofstream(src / "main.cpp") << "#include <iostream>\nint main() {}";
std::cout << "Created source file." << std::endl;
std::ofstream(config_file) << "{ \"version\": 1.0 }";
std::cout << "Created config file." << std::endl;
// 3. 复制文件 (备份)
fs::path backup_dir = root / "backup";
fs::create_directory(backup_dir);
// 将 config.json 复制到 backup/ 目录,如果存在则覆盖
fs::copy_file(config_file, backup_dir / "config.json.bak",
fs::copy_options::overwrite_existing);
std::cout << "Created backup." << std::endl;
}
catch (const fs::filesystem_error& e)
{
std::cerr << "Error: " << e.what() << '\n';
}
}
int main()
{
initialize_project("MyTestProject");
return 0;
}
2. 删除文件与目录
在 C++17 std::filesystem 中,删除操作(Delete)分为删除单个文件、删除空目录以及递归删除目录树。处理删除操作时,权限和目标是否存在是需要特别注意的两个方面。
2.1 删除文件和空目录
删除文件是最基础的删除操作。使用的函数是remove:
1 | bool remove(const std::filesystem::path& p); |
- 功能:删除路径
p指向的文件或目录。 - 返回值:
- 如果文件被成功删除,返回
true。 - 如果文件不存在,返回
false。 - 注意:如果
p是一个非空目录,行为是未定义的(通常情况下会失败并返回false,或抛出异常,具体取决于实现,但标准建议只用于删除文件或空目录)。
- 如果文件被成功删除,返回
- 异常:如果不使用
error_code版本,失败时会抛出filesystem_error异常。
1 |
|
虽然 remove 可以删除空目录,但在语义上,有时我们需要明确仅当目录为空时才删除。严格来说,C++17 没有单独提供名为 remove_empty_directory 的函数。std::remove 就是删除空目录的标准函数。
remove(p) 在 p 是目录时,只会在目录为空的情况下成功删除。并且remove 不会递归,如果目录里有东西,它会拒绝删除(失败)。
1 | fs::path dir_path = "temp_data"; |
2.2 删除所有内容
删除所有内容这是实际开发中最常用的删除函数,因为它足够智能。函数为remove_all:
1 | uintmax_t remove_all(const std::filesystem::path& p); |
- 功能:递归地删除路径
p指向的内容(包括所有子目录和文件)。 - 行为:
- 如果
p是一个文件,等同于remove。 - 如果
p是一个目录,它会删除该目录下的所有内容(递归),最后删除目录本身。 - 如果
p是一个符号链接,删除符号链接本身(不删除指向的目标)。
- 如果
- 返回值:
- 返回被删除的文件和目录的总数量。
- 如果
p不存在,返回0。
- 性能注意:
remove_all需要遍历目录树,对于包含大量文件的深层目录,耗时可能较长。
1 | // 无论 "build" 是一个文件、空目录还是包含成千上万文件的复杂目录结构 |
下面是一个展示如何安全、优雅地处理删除场景的函数,包含错误处理和重试机制(针对 Windows 占用问题的一个简单模拟)。
1 |
|
3. 读取与遍历
在 C++17 std::filesystem 中,“查”主要包含三个层面的操作:
- 读取属性:获取文件的大小、权限、修改时间等元数据。
- 读取内容:读取文本文件内容(注:这通常仍结合传统流,但
std::filesystem提供了路径支持)。 - 遍历目录:列出文件夹下的文件和子目录。
std::filesystem 的核心在于对文件属性的抽象,主要通过 file_status 和各种查询函数实现,这个在前文中已经做了非常细致的讲解 文件状态和属性
3.1 遍历目录
目录遍历这是 std::filesystem 最强大的功能之一,替代了老旧的 POSIX opendir / Windows FindFirstFile API。
3.1.1 单层遍历
单层目录遍历使用的类为std::filesystem::directory_iterator,构造函数如下:
1 | // 1. 默认构造 |
使用这个类可以遍历指定目录下的直接条目(不包括子目录中的文件)。示例代码:
1 | void list_current_directory() |
3.1.2 递归遍历
std::filesystem::recursive_directory_iterator 是 C++17 标准库 <filesystem> 中提供的一个迭代器类,用于递归地遍历目录树及其所有子目录的内容。
1 | recursive_directory_iterator() noexcept; |
默认构造函数:创建一个结束迭代器,通常用作遍历结束的判断条件。
带路径的构造函数:
p: 要遍历的目录路径。
详细选项信息可查询在线文档。opts: 遍历选项(例如是否跳过符号链接、是否允许权限错误等)ec: 错误码,用于非抛出异常的重载。
在这个类中还为我们提供了一组用于控制递归层级的 API 函数:
跳过特定子目录
1
void pop();
如果当前迭代器指向的是一个目录,调用
pop()会跳过该目录下的所有内容,直接返回到该目录的父目录,并继续遍历。注意:这个函数的行为实际上是结束对当前目录的遍历,常用于跳过特定子目录。
使用场景:已经进入了一个目录(或者正在深度遍历中),这是想强制停止当前的层级,直接回溯到上一层。
禁用递归进入
1
void disable_recursion_pending();
这是一个非常实用的函数。当你遍历到一个目录时,如果调用此函数,迭代器在下一次递增时将不会进入该目录(尽管
directory_entry::is_directory()返回 true),而是像对待普通文件一样跳过它。使用场景:在遍历列表时,发现一个目录,想在后续遍历时忽略它(把它当作普通文件处理),无进入。
深度查询
1
int depth() const;
返回当前迭代器在目录树中的深度。起始目录深度为 0,其子目录为 1,以此类推。
递归遍历目录示例代码:
1 |
|
注意:recursive_directory_iterator 默认不会跟随符号链接进入子目录,以防止无限循环。如需跟随,需构造时传入 directory_options::follow_directory_symlink 选项。
3.2 过滤器与查找
C++17 文件系统库本身不提供 find 或 filter 函数,但利用迭代器和 C++ 算法库可以轻松实现。
示例:查找所有
.cpp文件
1 |
|
4. 文件修改
在 std::filesystem 中,修改的操作主要体现为对文件系统对象的重命名、移动以及属性修改。
4.1 重命名和移动
在底层系统中,重命名和移动通常是同一个系统调用。只要源文件和目标文件在同一个文件系统(挂载点)上,移动操作仅仅是修改目录元数据,速度极快,且不涉及文件数据的物理拷贝。
1 | bool rename(const std::filesystem::path& old_p, const std::filesystem::path& new_p); |
- 功能:
- 将
old_p重命名为new_p。 - 如果
new_p路径中包含不同的目录名,则实现了移动效果。
- 将
- 重要特性:
- 覆盖行为:如果
new_p已经存在,它的行为取决于编译器和操作系统。- 在大多数 POSIX 系统上,如果
new_p是非空目录,会失败;如果是空目录或文件,通常原子性地替换。 - 标准建议:如果
new_p存在,结果是实现定义的。为了代码健壮性,强烈建议在移动前检查目标是否存在,或者确保期望覆盖。
- 在大多数 POSIX 系统上,如果
- 跨卷移动:如果源和目标在不同的磁盘分区(挂载点),单纯的
rename可能会失败(错误码为EXDEV)。标准的rename不执行跨文件系统的数据拷贝。
- 覆盖行为:如果
示例:重命名文件
1 |
|
示例:移动文件到新目录
1 | void move_file_to_folder() |
4.2 文件权限修改
修改文件的读写执行权限是“改”的另一重要部分。使用的函数如下:
1 | void permissions(const std::filesystem::path& p, perms prms, perm_options opts = perm_options::replace); |
perms:枚举类型,如owner_read,group_write,all_exec等。perm_options(重要):控制如何应用新权限,默认是replace(替换)。还可以使用:add:在现有权限基础上添加(按位或)。remove:在现有权限基础上移除(按位异或/取反)。nofollow:如果是符号链接,不修改指向的目标,只修改链接本身(支持情况视系统而定)。
示例:将文件设置为只读
1 | void make_readonly(const fs::path& p) |
5. 实战示例:清理工具
结合前面讲过的知识,我们可以写一个清理临时文件的小工具:
1 |
|



















