如何使用C++17标准库实现跨平台文件复制

在现代C++中,std::filesystem(在C++17中正式加入)为文件系统操作提供了一套统一、跨平台的接口。本文将演示如何利用它实现一个简易的文件复制工具,并讨论一些常见的错误处理和性能优化技巧。

1. 环境准备

  • 编译器:gcc 8+ / clang 9+ / MSVC 2017+,均支持std::filesystem
  • 标准选项:-std=c++17(或更高)。

2. 基本思路

复制文件的核心步骤如下:

  1. 打开源文件(std::ifstream)和目标文件(std::ofstream)。
  2. 以二进制模式读写,缓冲区可以是固定大小(如 8KB)。
  3. 逐块复制,直到源文件结束。
  4. 处理异常:文件不存在、权限不足、磁盘空间不足等。

使用std::filesystem可简化路径检查、文件属性获取和错误报告。

3. 代码实现

#include <filesystem>
#include <fstream>
#include <iostream>
#include <vector>

namespace fs = std::filesystem;

// 把单个文件从src复制到dst
bool copy_file(const fs::path& src, const fs::path& dst, std::error_code& ec) {
    // 确认源文件存在且可读
    if (!fs::exists(src, ec) || !fs::is_regular_file(src, ec)) {
        ec = std::make_error_code(std::errc::no_such_file_or_directory);
        return false;
    }

    // 创建目标目录(如果不存在)
    fs::path dst_dir = dst.parent_path();
    if (!dst_dir.empty() && !fs::exists(dst_dir, ec)) {
        fs::create_directories(dst_dir, ec);
        if (ec) return false;
    }

    std::ifstream in(src, std::ios::binary);
    std::ofstream out(dst, std::ios::binary);
    if (!in) { ec = std::make_error_code(std::errc::io_error); return false; }
    if (!out) { ec = std::make_error_code(std::errc::io_error); return false; }

    const std::size_t buffer_size = 8192; // 8KB
    std::vector <char> buffer(buffer_size);

    while (in) {
        in.read(buffer.data(), buffer_size);
        std::streamsize bytes = in.gcount();
        if (bytes > 0) out.write(buffer.data(), bytes);
        if (!out) { ec = std::make_error_code(std::errc::io_error); return false; }
    }

    return true;
}

// 复制目录下的所有文件(递归)
bool copy_directory(const fs::path& src, const fs::path& dst, std::error_code& ec) {
    if (!fs::exists(src, ec) || !fs::is_directory(src, ec)) {
        ec = std::make_error_code(std::errc::not_a_directory);
        return false;
    }

    for (auto& entry : fs::recursive_directory_iterator(src, ec)) {
        if (ec) return false;
        const fs::path& src_path = entry.path();
        fs::path relative = fs::relative(src_path, src, ec);
        if (ec) return false;
        fs::path dst_path = dst / relative;

        if (fs::is_directory(src_path, ec)) {
            fs::create_directory(dst_path, ec);
            if (ec) return false;
        } else if (fs::is_regular_file(src_path, ec)) {
            if (!copy_file(src_path, dst_path, ec)) return false;
        }
    }
    return true;
}

int main() {
    std::error_code ec;
    fs::path src = "src_folder";
    fs::path dst = "dst_folder";

    if (copy_directory(src, dst, ec)) {
        std::cout << "复制完成!\n";
    } else {
        std::cerr << "复制失败: " << ec.message() << "\n";
    }
    return 0;
}

4. 关键细节说明

  1. 异常 vs. error_code
    std::filesystem 默认使用异常机制,fs::existsfs::create_directories 等函数会抛出 std::filesystem::filesystem_error。若想避免异常,传入 std::error_code &ec 参数即可。本文统一使用 error_code,更易于错误聚合和日志记录。

  2. 缓冲区大小
    8KB 是一个折衷的大小;对磁盘 I/O 性能影响不大;若在网络文件系统上,可根据带宽调整。

  3. 权限与所有权
    默认复制后,目标文件拥有调用进程的用户权限。若需要保留原文件的权限和时间戳,可在复制完成后使用 fs::permissionsfs::last_write_time 等函数同步属性。

  4. 符号链接与特殊文件
    fs::recursive_directory_iterator 会遍历符号链接。默认情况下,is_directory 会返回链接指向的目录。若想复制链接本身而不是目标,可使用 fs::directory_options::skip_permission_denied 或自行处理 is_symlink

  5. 性能优化

    • 内存映射mmap)适用于大文件复制,但与标准库兼容性差。
    • 多线程:将目录拆分为多线程任务,可显著提升磁盘 I/O 并行度,但需注意同步和锁的开销。

5. 常见错误处理

场景 典型错误码 解决办法
源文件不存在 ENOENT 检查路径拼写,使用绝对路径
目标目录不可写 EACCES 确认用户权限,使用 sudo 或修改权限
磁盘空间不足 ENOSPC 清理磁盘,或限制复制范围
链接循环 ELOOP 设置 directory_options::follow_directory_symlinkskip

6. 结语

利用C++17的std::filesystem可以极大地简化跨平台文件系统操作,并保持代码简洁可读。通过上述示例,你可以快速搭建自己的文件复制工具,并根据需求进一步扩展功能,例如支持增量同步、文件压缩或网络传输。希望这篇文章对你在实际项目中的文件操作有所帮助。

发表评论