如果只依靠基础的文件流来复制一个文件,我们需要同时打开源文件和目标文件,再写循环把内容一点点搬过去,写起来比较繁琐。从 C++17 标准开始,无论是复制、删除还是重命名单个文件,我们都可以直接调用 <filesystem> 标准库中的现成函数一行搞定。
在日常开发中,管理单个普通文件最常用的是 fs::copy_file()、fs::remove() 和 fs::rename() 这 3 个函数。这里的 fs 是命名空间 std::filesystem 的常用别名。
使用 copy_file() 复制文件
在 C++ 中,调用 fs::copy_file() 复制普通文件时,我们需要区分 “目标文件原本不存在” 和 “目标文件已经存在” 这两种情况。
1. 复制普通文件
当目标位置还没有同名文件时,我们直接向 fs::copy_file() 传入源文件路径和目标文件路径即可。
语法:
fs::copy_file(source, target)说明:
这两个路径参数既可以传入字符串,也可以传入 fs::path 路径对象,具体说明如下:
- source(必选):源文件路径,表示 “从哪里复制”。该文件必须已经存在,并且必须是一个普通文件。
- target(必选):目标文件路径,表示 “复制到哪里”。注意
source在前、target在后,顺序不要写反了。
如果复制成功,fs::copy_file() 会返回 true。另外要注意,该函数只能复制单个普通文件。如果想要复制整个文件夹,我们需要使用 fs::copy() 函数。
示例 1:复制单个普通文件
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
int main() {
fs::path source = "source.txt";
fs::path target = "target.txt";
// 准备源文件 source.txt
std::ofstream outFile(source);
outFile << "绿叶网 lvyenet.com\n";
outFile.close();
bool copied = fs::copy_file(source, target);
std::cout << std::boolalpha;
std::cout << "是否复制成功:" << copied << '\n';
// 读取目标文件验证内容
std::ifstream inFile(target);
std::string line;
std::getline(inFile, line);
std::cout << "目标文件内容:" << line << '\n';
return 0;
}运行结果如下。
是否复制成功:true
目标文件内容:绿叶网 lvyenet.com分析:
在这个例子中,我们先写入了源文件 source.txt,随后调用 fs::copy_file(source, target) 将它复制为 target.txt。最后读取 target.txt 可以看到,里面的内容和源文件一模一样。
这里有一个默认保护规则:如果磁盘上已经存在 target.txt,比如我们在本地连续运行两次上面这段代码,双参数的 fs::copy_file(source, target) 就会报错拒绝复制。如果确实需要覆盖已有的目标文件,就要用到下面的覆盖策略。
2. 指定覆盖策略(copy_options)
当目标文件可能已经存在时,我们可以通过第 3 个参数传入 fs::copy_options 枚举值,明确告诉程序遇到同名目标文件时应该怎么处理。
语法:
fs::copy_file(source, target, options)说明:
第 3 个参数 options 用于指定目标文件已存在时的处理策略,常用的 4 种枚举值如下表所示。
| 策略枚举值 | 目标文件已存在时的行为 |
|---|---|
| fs::copy_options::none | 默认策略,直接报错,不覆盖已有目标文件 |
| fs::copy_options::skip_existing | 跳过复制并保留已有目标文件,不报错且返回 false |
| fs::copy_options::overwrite_existing | 直接覆盖已有的目标文件,返回 true |
| fs::copy_options::update_existing | 仅当源文件的最后修改时间比目标文件更新时才覆盖 |
对于上表中的 4 种策略,小伙伴们不需要死记硬背,重点掌握最常用的 overwrite_existing(强制覆盖)和 skip_existing(跳过已有文件)就可以了。
示例 2:覆盖已有的目标文件
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
int main() {
fs::path source = "source.txt";
fs::path target = "target.txt";
// 准备源文件和已有的目标文件
std::ofstream srcFile(source);
srcFile << "new content\n";
srcFile.close();
std::ofstream dstFile(target);
dstFile << "old content\n";
dstFile.close();
// 强制覆盖已有的目标文件
auto opt = fs::copy_options::overwrite_existing;
bool copied = fs::copy_file(source, target, opt);
std::cout << std::boolalpha;
std::cout << "是否覆盖复制:" << copied << '\n';
std::ifstream inFile(target);
std::string line;
std::getline(inFile, line);
std::cout << "覆盖后内容:" << line << '\n';
return 0;
}运行结果如下。
是否覆盖复制:true
覆盖后内容:new content分析:
在上面的代码中,target.txt 原本已经存在,里面保存的是 "old content"。调用 fs::copy_file() 时传入 fs::copy_options::overwrite_existing 后,程序使用 source.txt 的新内容覆盖了 target.txt,并返回 true。
对于这个例子来说,如果把策略改为 skip_existing,程序同样不会报错。此时 copied 会返回 false,target.txt 里保留的依然是原来的 "old content"。
使用 remove() 删除文件
在 C++ 中,如果想要把某个文件从磁盘上删除,我们可以调用 fs::remove() 函数。
语法:
fs::remove(path)
fs::remove(path, ec)说明:
fs::remove() 接收要删除的文件路径 path 以及可选的错误码对象 ec,返回值规则如下:
- 目标文件存在且成功删除:返回
true。 - 目标文件原本就不存在:返回
false,并且不会报错。如果传入了ec,ec依然保持无错误状态。 - 删除过程本身出错:例如没有操作权限,或者该文件正被其他软件占用。此时单参数版本会抛出异常,双参数版本则会将错误原因记录到
ec中。
顺带一提,前面的 fs::copy_file() 和后面的 fs::rename() 同样支持在末尾传入 ec 参数进行免异常检查。
示例 3:删除指定文件
#include <filesystem>
#include <fstream>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
int main() {
fs::path path = "delete_demo.txt";
// 创建用于测试删除的文件
std::ofstream outFile(path);
outFile << "temporary data\n";
outFile.close();
std::error_code ec;
bool first = fs::remove(path, ec); // 第 1 次删除:文件存在
bool second = fs::remove(path, ec); // 第 2 次删除:文件已不存在
std::cout << std::boolalpha;
std::cout << "第 1 次删除结果:" << first << '\n';
std::cout << "第 2 次删除结果:" << second << '\n';
if (!ec) {
std::cout << "两次删除过程均未出错\n";
}
return 0;
}运行结果如下。
第 1 次删除结果:true
第 2 次删除结果:false
两次删除过程均未出错分析:
当我们第 1 次调用 fs::remove(path, ec) 时,delete_demo.txt 确实存在,文件被成功删除并返回 true。第 2 次再删同一个路径时,因为文件刚才已经被删掉了,所以返回 false。同时由于文件不存在不算系统故障,!ec 依然成立。
另外,fs::remove() 不仅能删除单个普通文件,还可以用来删除空文件夹。如果要删除装有内容的非空文件夹,则需要配合下一节介绍的 fs::remove_all() 函数。
注意 调用 fs::remove() 删除的文件不会进入电脑的 “回收站”,而是直接从磁盘上彻底移除。小伙伴们在编写和运行删除代码时,记得先确认目标路径无误,只对专门新建的测试文件执行删除操作。
使用 rename() 重命名与移动文件
在日常文件管理中,除了复制和删除之外,我们还经常需要给文件 “改名字”,或者把文件 “移动到另一个文件夹”。在 <filesystem> 库中,这两件事都可以调用 fs::rename() 函数实现。
语法:
fs::rename(oldPath, newPath)
fs::rename(oldPath, newPath, ec)说明:
oldPath 是原文件路径,newPath 是新文件路径。fs::rename() 没有返回值,根据 newPath 所在位置的不同,它可以实现以下 2 种操作:
- 原地重命名文件:当两个路径位于同一个目录下时,比如
fs::rename("draft.txt", "final.txt");,会直接把文件名从draft.txt改为final.txt。 - 移动文件位置:当
newPath指向另一个已经存在的文件夹下时,比如fs::rename("final.txt", "backup/final.txt");,会把文件直接移入backup文件夹中,不需要先复制再删除。
示例 4:重命名文件
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path oldPath = "draft.txt";
fs::path newPath = "final.txt";
// 创建原文件并清理同名新文件
std::ofstream outFile(oldPath);
outFile << "report data\n";
outFile.close();
fs::remove(newPath);
// 重命名文件
fs::rename(oldPath, newPath);
bool oldExists = fs::exists(oldPath);
bool newExists = fs::exists(newPath);
std::cout << std::boolalpha;
std::cout << "draft.txt 是否存在:" << oldExists << '\n';
std::cout << "final.txt 是否存在:" << newExists << '\n';
return 0;
}运行结果如下。
draft.txt 是否存在:false
final.txt 是否存在:true分析:
fs::rename(oldPath, newPath); 表示将原本的 draft.txt 直接改名为 final.txt。改名结束后,我们调用 fs::exists() 检查可以看到,原来的 draft.txt 已不存在,而新的 final.txt 存在且保留了原有的文件内容。
如果我们想把 final.txt 移动到已经建好的子文件夹 backup 中,只需要把第 2 个参数写成目标文件夹下的路径即可:
// 将文件移动到已有的 backup 文件夹下
fs::rename("final.txt", "backup/final.txt");需要注意的是,使用 fs::rename() 移动文件时,目标文件夹 backup 必须已经存在,该函数不会自动新建缺失的文件夹。另外,如果目标位置已经有同名文件,fs::rename() 默认会直接覆盖它,这一点与 fs::copy_file() 默认报错不覆盖的行为正好相反。
常见问题
1. 为什么使用 fs::rename() 把文件从 C 盘移动到 D 盘会报错失败?
这是因为 fs::rename() 移动文件时,底层只是修改了文件系统里的目录记录,并没有真正复制文件数据,所以要求原路径和新路径必须位于同一个磁盘分区内。比如在 Windows 下把文件从 C 盘移到 D 盘,操作系统没法只改目录记录就跨盘移动,此时就会报错失败。
遇到需要跨磁盘分区移动文件的场景时,大家分两步处理即可:先调用 fs::copy_file() 把文件复制到目标盘,确认复制成功后再调用 fs::remove() 把原文件删除。
