实战项目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_t或int64_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