C++ 复制与删除文件

如果只依靠基础的文件流来复制一个文件,我们需要同时打开源文件和目标文件,再写循环把内容一点点搬过去,写起来比较繁琐。从 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 种枚举值如下表所示。

copy_options 覆盖策略
策略枚举值 目标文件已存在时的行为
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() 把原文件删除。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号