实战项目1:实现一个简易文件拷贝工具,支持进度显示与错误处理

文件拷贝,听起来很简单对吧?

不就是打开一个文件,读出来,再写到另一个文件里?

嗯,理论上是这样。但我在实际项目中做过不少文件传输工具,发现坑其实不少。比如大文件拷贝到一半磁盘满了怎么办?源文件被其他进程锁住了怎么处理?用户想看到进度条,你总不能让他干等吧?

今天我们就手写一个简易的文件拷贝工具。它不复杂,但足够实用。我会把我在嵌入式开发中踩过的坑,一并告诉你。

项目需求分析

先明确我们要做什么:

  • 支持任意大小的文件拷贝(文本、二进制都行)
  • 实时显示拷贝进度(百分比)
  • 完善的错误处理(文件不存在、权限不足、磁盘满等)
  • 命令行参数指定源文件和目标文件

说白了,就是一个简化版的 cp 命令,但多了进度显示。

核心设计思路

整个程序的核心逻辑,我画了一张流程图,你看一眼就明白了:

开始 解析命令行参数(源文件、目标文件) 以只读方式打开源文件 创建/覆盖目标文件

上面只是前半部分。拷贝循环和错误处理,我放在下面这张图里:

循环:读取数据块 → 写入目标文件 更新进度显示(百分比) 文件读完? 关闭源文件和目标文件 错误处理:打印错误信息

完整代码实现

好,直接上代码。我习惯把功能拆成几个小函数,这样调试起来方便:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>

#define BUFFER_SIZE (1024 * 64)  // 64KB缓冲区
#define PROGRESS_BAR_WIDTH 50    // 进度条宽度

// 获取文件大小
long get_file_size(const char *path) {
    struct stat st;
    if (stat(path, &st) != 0) {
        return -1;
    }
    return st.st_size;
}

// 显示进度条
void show_progress(long current, long total) {
    if (total == 0) return;
    
    int percent = (int)(current * 100 / total);
    int bar_len = percent * PROGRESS_BAR_WIDTH / 100;
    
    printf("\r[");
    for (int i = 0; i < PROGRESS_BAR_WIDTH; i++) {
        if (i < bar_len) putchar('=');
        else if (i == bar_len) putchar('>');
        else putchar(' ');
    }
    printf("] %3d%% (%ld/%ld bytes)", percent, current, total);
    fflush(stdout);
}

// 核心拷贝函数
int copy_file(const char *src, const char *dst) {
    FILE *fp_src = NULL, *fp_dst = NULL;
    char *buffer = NULL;
    long total_size, copied = 0;
    size_t bytes_read;
    int ret = -1;
    
    // 获取源文件大小
    total_size = get_file_size(src);
    if (total_size < 0) {
        fprintf(stderr, "错误:无法获取源文件信息 - %s\n", strerror(errno));
        return -1;
    }
    
    // 打开源文件
    fp_src = fopen(src, "rb");
    if (!fp_src) {
        fprintf(stderr, "错误:无法打开源文件 '%s' - %s\n", src, strerror(errno));
        return -1;
    }
    
    // 创建目标文件
    fp_dst = fopen(dst, "wb");
    if (!fp_dst) {
        fprintf(stderr, "错误:无法创建目标文件 '%s' - %s\n", dst, strerror(errno));
        fclose(fp_src);
        return -1;
    }
    
    // 分配缓冲区
    buffer = (char*)malloc(BUFFER_SIZE);
    if (!buffer) {
        fprintf(stderr, "错误:内存分配失败\n");
        fclose(fp_src);
        fclose(fp_dst);
        return -1;
    }
    
    printf("开始拷贝:%s → %s\n", src, dst);
    printf("文件大小:%ld bytes\n\n", total_size);
    
    // 循环读写
    while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fp_src)) > 0) {
        size_t bytes_written = fwrite(buffer, 1, bytes_read, fp_dst);
        if (bytes_written != bytes_read) {
            fprintf(stderr, "\n错误:写入失败 - %s\n", strerror(errno));
            goto cleanup;
        }
        copied += bytes_written;
        show_progress(copied, total_size);
    }
    
    // 检查读取是否出错
    if (ferror(fp_src)) {
        fprintf(stderr, "\n错误:读取源文件失败 - %s\n", strerror(errno));
        goto cleanup;
    }
    
    printf("\n\n拷贝完成!共 %ld bytes\n", copied);
    ret = 0;
    
cleanup:
    if (buffer) free(buffer);
    if (fp_src) fclose(fp_src);
    if (fp_dst) fclose(fp_dst);
    return ret;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "用法:%s <源文件> <目标文件>\n", argv[0]);
        return 1;
    }
    
    if (copy_file(argv[1], argv[2]) != 0) {
        fprintf(stderr, "拷贝失败!\n");
        return 1;
    }
    
    return 0;
}

关键点解析

这段代码里,有几个地方我想特别说一下:

  • 缓冲区大小:我选了64KB。为什么不是1KB或1MB?我在嵌入式设备上试过,64KB是个比较平衡的值——太大浪费内存,太小频繁IO反而慢。
  • 进度显示:用 \r 回车符覆盖当前行,这样进度条就在原地刷新,不会刷屏。你想想看,如果每拷贝一点就换行,那屏幕不得炸了?
  • 错误处理:每个可能失败的操作都检查了返回值。我曾经在一个项目里偷懒没检查 fwrite 的返回值,结果磁盘满了程序还在傻傻地跑,用户以为拷贝成功了……嗯,从那以后我再也不敢省这一步了。

避坑指南

我曾经踩过的坑,你千万别再踩:

  • 文本模式 vs 二进制模式:Windows 下文本模式会做换行符转换(\n → \r\n),导致拷贝二进制文件(如图片、可执行文件)时数据损坏。所以一定要用 "rb""wb"
  • 大文件进度溢出:如果文件超过2GB,用 int 存文件大小会溢出。我用了 long,在64位系统上够用。如果你要拷贝超大型文件,建议用 off_tint64_t
  • 缓冲区未刷新:进度显示用了 fflush(stdout),否则在重定向输出到文件时,进度信息可能被缓存,用户看不到实时更新。

运行效果演示

假设我们有一个 100MB 的文件 bigfile.bin,运行命令:

$ ./filecopy bigfile.bin bigfile_copy.bin
开始拷贝:bigfile.bin → bigfile_copy.bin
文件大小:104857600 bytes

[==================================================] 100% (104857600/104857600 bytes)

拷贝完成!共 104857600 bytes

进度条会从 0% 一直走到 100%,整个过程一目了然。

扩展思考

这个工具虽然简单,但已经具备了实用价值。如果你想继续完善,可以考虑:

  • 支持目录递归拷贝
  • 断点续传功能
  • 多线程并行拷贝(大文件时能提速不少)
  • 校验和验证(拷贝后对比MD5,确保数据完整)

我个人觉得,文件操作是C语言里最实用的技能之一。你想想看,从嵌入式设备到服务器,哪个系统不需要读写文件?把这个基本功练扎实了,后面学数据库、网络编程都会轻松很多。

小提示: 如果你在 Linux 下测试,可以用 dd if=/dev/zero of=testfile bs=1M count=100 快速生成一个 100MB 的测试文件。


公众号:蓝海资料掘金营,微信deep3321