C语言入门:C 语言文件复制

第一章:C 语言文件操作基础概念

1.1 文件是什么?

在 C 语言中,文件是存储在外部介质(如硬盘)上的一系列数据的集合。可以是文本文件(如.txt,人类可读)或二进制文件(如.exe、图片、音频,计算机直接读取)。
核心特点:文件操作需要通过 “文件指针”(FILE *)作为 “中间人”,告诉程序 “当前操作哪个文件”。

1.2 文件打开与关闭:一切操作的起点和终点
  • 打开文件:fopen()函数
    格式:FILE *fopen(const char *filename, const char *mode);

    • filename:文件路径(如"D:\\test.txt""./data.bin")。
    • mode:打开模式(必须记牢!):
      模式含义文本文件 / 二进制文件
      r只读(文件必须存在)文本
      w写入(不存在则创建,存在则清空)文本
      a追加(不存在则创建,存在则在末尾添加)文本
      rb只读(二进制模式)二进制
      wb写入(二进制模式)二进制
      ab追加(二进制模式)二进制
      r+读写(文件必须存在)文本
      w+读写(创建或清空)文本
      a+读写(追加,不清空)文本
      rb+读写(二进制模式)二进制
      重点:复制文件时,源文件用rrb,目标文件用wwb(若需保留目标文件原有内容并追加,用a,但复制通常是全新覆盖,所以用w)。

    错误处理:必须检查fopen是否返回NULL(文件打开失败,比如路径错误、无权限):

    FILE *src_file = fopen("source.txt", "r");
    if (src_file == NULL) {
        printf("无法打开源文件!\n");
        exit(1); // 终止程序
    }
    
  • 关闭文件:fclose()函数
    格式:int fclose(FILE *stream);

    • 必须关闭!否则可能导致数据未写入磁盘(缓冲区未刷新)或资源泄漏。
    • 返回值:成功返回0,失败返回EOF(需用ferror()进一步检查错误)。
第二章:文件复制的核心逻辑:“读 - 写” 循环

文件复制的本质是一个循环过程:
从源文件读取数据 → 写入目标文件 → 重复直到源文件结束
根据读取单位的不同,分为 “字符级复制” 和 “块级复制”,下面详细讲解。

2.1 字符级复制:适合文本文件(逐个字符搬运)

使用fgetc()(读字符)和fputc()(写字符),每次处理一个字节(文本文件中一个字符通常对应一个字节,如 ASCII 字符)。
步骤示例

  1. 打开源文件(读模式)和目标文件(写模式)。
  2. fgetc从源文件读一个字符,直到遇到EOF(文件结束标志)。
  3. fputc将字符写入目标文件。
  4. 关闭两个文件。

代码模板

#include <stdio.h>
#include <stdlib.h> // 用于exit()

void copy_file_char(const char *src_path, const char *dst_path) {
    FILE *src = fopen(src_path, "r");
    FILE *dst = fopen(dst_path, "w");
    if (src == NULL || dst == NULL) {
        printf("文件打开失败!\n");
        if (src) fclose(src); // 确保失败时关闭已打开的文件
        if (dst) fclose(dst);
        exit(1);
    }

    int ch; // 用int存储,因为fgetc返回值可能是EOF(-1),char无法表示
    while ((ch = fgetc(src)) != EOF) { // 循环读取直到文件结束
        fputc(ch, dst); // 写入目标文件
    }

    fclose(src);
    fclose(dst);
    printf("字符级复制完成!\n");
}

int main() {
    copy_file_char("source.txt", "destination.txt");
    return 0;
}

注意事项

  • 文本文件中,fgetc会自动处理换行符(如 Windows 的\r\n在读取时转为\n,写入时反之),但二进制文件不能用这种方式,因为二进制数据中可能包含EOF对应的数值(-1),导致提前终止。
  • 效率问题:每次读写一个字符,会频繁调用操作系统接口,效率低,不适合大文件(比如 1GB 的文件需要 10 亿次调用)。
2.2 块级复制:适合所有文件(批量搬运,效率更高)

使用fread()(读块)和fwrite()(写块),每次读取一个缓冲区大小的数据(如 1KB、4KB),减少系统调用次数。
核心函数

  • size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
    • stream读取count个大小为size的块,存入ptr指向的缓冲区,返回实际读取的块数。
  • size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
    • stream写入count个大小为size的块,从ptr读取数据,返回实际写入的块数。

步骤示例

  1. 打开源文件(二进制读模式rb)和目标文件(二进制写模式wb)——二进制模式是关键,避免系统自动转换字符(如换行符),确保数据原样复制
  2. 创建缓冲区(如char buffer[4096];,通常用 4KB 或 8KB,匹配磁盘块大小)。
  3. 循环:
    • 从源文件读buffer大小的数据到缓冲区。
    • 将缓冲区数据写入目标文件。
    • 直到读取的字节数为 0(文件结束)。
  4. 关闭文件。

代码模板

#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 4096 // 缓冲区大小,可调整(通常为4KB的倍数)

void copy_file_block(const char *src_path, const char *dst_path) {
    FILE *src = fopen(src_path, "rb"); // 二进制读
    FILE *dst = fopen(dst_path, "wb"); // 二进制写
    if (src == NULL || dst == NULL) {
        printf("文件打开失败!\n");
        if (src) fclose(src);
        if (dst) fclose(dst);
        exit(1);
    }

    char buffer[BUFFER_SIZE];
    size_t bytes_read; // 记录实际读取的字节数

    while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src)) > 0) { // 每次读BUFFER_SIZE字节
        fwrite(buffer, 1, bytes_read, dst); // 写入刚读取的字节数(最后一次可能不足BUFFER_SIZE)
    }

    fclose(src);
    fclose(dst);
    printf("块级复制完成!\n");
}

int main() {
    copy_file_block("source.bin", "destination.bin");
    return 0;
}

优势

  • 适用于所有文件类型(文本、二进制),因为二进制模式下不会误解数据(比如不会把0x0A0x0D当作换行符转换)。
  • 效率高:假设缓冲区是 4KB,1GB 文件只需约 262,144 次读写,比字符级的 10 亿次快太多。
第三章:深入理解文件复制的关键细节
3.1 文本文件 vs 二进制文件:复制的区别
  • 文本文件

    • 人类可读,由字符组成(如 ASCII、UTF-8)。
    • 在不同系统中换行符不同(Windows:\r\n,Linux:\n,Mac:\r)。
    • 使用fgetc/fputc时,C 语言会自动转换换行符(比如读 Windows 文件时,\r\n会被转为\n存储;写入时,\n会转为\r\n)。
    • 注意:如果文本文件包含非文本数据(如二进制内容),可能导致错误(比如fgetc遇到EOF值会提前终止),所以复制文本文件也推荐用二进制模式,避免换行符转换干扰。
  • 二进制文件

    • 直接存储二进制数据(如图片、可执行文件),不能用文本编辑器正确查看。
    • 必须用二进制模式(rb/wb)打开,避免系统自动转换字符,确保每个字节都被原样复制。
    • fread/fwrite按字节块操作,不会误解数据中的EOF(因为判断依据是读取的字节数是否为 0,而非某个特定值)。

结论通用的文件复制应使用二进制模式和块级复制,既能处理所有文件类型,又能保证效率。

3.2 缓冲区的作用:为什么不能没有它?
  • 缓冲区是内存中的一块区域,用于暂存数据。
  • 没有缓冲区时(比如每次读写 1 字节):
    • 每次fgetc/fputc都要调用操作系统的底层文件操作,涉及用户态和内核态的切换,开销极大。
  • 有缓冲区时
    • fread先将数据批量读入内存缓冲区,填满后再一次性写入目标文件的缓冲区,最后由操作系统批量写入磁盘。
    • 减少系统调用次数,提升效率(内存访问比磁盘 IO 快百万倍)。

缓冲区大小如何选?

  • 通常设为 4096 字节(4KB),因为这是多数磁盘的块大小,与底层存储单元匹配,效率最佳。
  • 过大的缓冲区(如 1MB)可能占用过多内存,尤其是同时复制多个文件时;过小则效率不高。
3.3 文件结束的判断:EOF vs 读取字节数为 0
  • 文本文件(字符级复制):用fgetc返回EOF(-1)判断结束,但需注意:

    • 如果文件包含值为0xFF(二进制)的字节,在文本模式下可能被误判为EOF(因为EOF通常定义为 - 1,即二进制补码全 1)。
    • 所以文本文件也建议用二进制模式和块级复制,通过fread的返回值判断是否结束(返回 0 时结束)。
  • 二进制文件(块级复制):直接检查fread的返回值,当读取的字节数为 0 时,说明文件结束,不会被数据中的特定值干扰。

3.4 错误处理:比代码本身更重要

文件操作中可能遇到的错误包括:

  1. 源文件不存在(fopen返回NULL)。
  2. 目标文件无法创建(如目录不存在、无写入权限)。
  3. 读取或写入过程中磁盘空间不足。
  4. 文件被其他程序占用(无法打开)。

完善的错误处理代码示例

void copy_file_safe(const char *src_path, const char *dst_path) {
    FILE *src = fopen(src_path, "rb");
    if (src == NULL) {
        perror("打开源文件失败"); // 打印具体错误信息(如“没有那个文件或目录”)
        exit(1);
    }

    FILE *dst = fopen(dst_path, "wb");
    if (dst == NULL) {
        perror("创建目标文件失败");
        fclose(src);
        exit(1);
    }

    char buffer[BUFFER_SIZE];
    size_t bytes_read;

    while (1) {
        bytes_read = fread(buffer, 1, BUFFER_SIZE, src);
        if (bytes_read == 0) { // 正常结束或错误
            if (ferror(src)) { // 检查读取时是否出错
                perror("读取源文件时发生错误");
                break;
            }
            break; // 正常结束
        }
        if (fwrite(buffer, 1, bytes_read, dst) != bytes_read) { // 写入字节数不等于读取字节数,说明写入错误
            perror("写入目标文件时发生错误");
            break;
        }
    }

    fclose(src);
    fclose(dst);
    // 可根据是否发生错误输出不同信息
}

关键函数

  • perror(char *s):打印上次错误的具体信息(如 “权限被拒绝”“磁盘空间不足”),需包含stdio.h
  • ferror(FILE *stream):返回非零值表示流上发生了错误,需手动检查(因为fread/fwrite返回 0 可能是正常结束,也可能是错误)。
第四章:扩展应用与最佳实践
4.1 命令行参数实现文件复制(类似cp命令)

让程序支持通过命令行输入源文件和目标文件路径,如:
./copy.exe source.txt destination.txt

代码实现

#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 4096

int main(int argc, char *argv[]) {
    if (argc != 3) { // 检查参数数量是否正确(程序名+源文件+目标文件)
        printf("用法:%s 源文件 目标文件\n", argv[0]);
        return 1;
    }

    const char *src_path = argv[1];
    const char *dst_path = argv[2];

    FILE *src = fopen(src_path, "rb");
    if (src == NULL) {
        perror("打开源文件失败");
        return 1;
    }

    FILE *dst = fopen(dst_path, "wb");
    if (dst == NULL) {
        perror("创建目标文件失败");
        fclose(src);
        return 1;
    }

    char buffer[BUFFER_SIZE];
    size_t bytes_read;

    while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src)) > 0) {
        if (fwrite(buffer, 1, bytes_read, dst) != bytes_read) {
            perror("写入目标文件时发生错误");
            break;
        }
    }

    fclose(src);
    fclose(dst);
    printf("文件复制成功!\n");
    return 0;
}

注意

  • argc是命令行参数个数,argv是参数数组,argv[0]是程序名本身。
  • 这种写法更接近 Linux 的cp命令,方便用户在终端使用。
4.2 复制文件夹:文件复制的 “升级版”

C 语言标准库没有直接复制文件夹的函数,因为文件夹涉及递归遍历子文件和子目录。需借助:

  1. opendir()/readdir()遍历目录中的文件。
  2. mkdir()创建目标文件夹。
  3. 对每个文件调用文件复制函数。

伪代码逻辑

void copy_dir(const char *src_dir, const char *dst_dir) {
    // 创建目标目录
    mkdir(dst_dir); 

    // 打开源目录
    DIR *dir = opendir(src_dir);
    struct dirent *entry;

    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue; // 跳过当前目录和父目录
        }

        char src_path[1024], dst_path[1024];
        sprintf(src_path, "%s/%s", src_dir, entry->d_name);
        sprintf(dst_path, "%s/%s", dst_dir, entry->d_name);

        struct stat statbuf;
        stat(src_path, &statbuf);

        if (S_ISDIR(statbuf.st_mode)) { // 如果是子目录,递归复制
            copy_dir(src_path, dst_path);
        } else { // 如果是文件,调用文件复制函数
            copy_file_block(src_path, dst_path);
        }
    }

    closedir(dir);
}

需要的头文件

  • #include <dirent.h>(目录操作)
  • #include <sys/stat.h>(获取文件属性)
  • 注意:此代码为 Linux/macOS 版本,Windows 需用Windows API(如CreateFileFindFirstFile)实现,跨平台需额外处理。
4.3 大文件复制:性能优化关键点
  • 缓冲区大小:设为 4KB、8KB 或更大(如 1MB),但需注意内存占用(64 位系统可适当增大)。
  • 多线程 / 异步 IO:C 语言标准库不支持多线程文件复制,但可通过操作系统 API(如 Linux 的pthreads)实现,但需注意文件指针的线程安全(每个线程操作独立的文件指针)。
  • 避免频繁打开 / 关闭文件:一次性打开源文件和目标文件,完成整个复制流程后再关闭。
第五章:常见错误与解决方案
错误现象可能原因解决方案
目标文件为空源文件打开失败,未进入读写循环检查fopen是否返回NULL,添加错误处理
复制后文件内容错误使用文本模式复制二进制文件改用二进制模式(rb/wb
程序崩溃未检查fgetc返回值直接转charint存储fgetc结果,避免溢出
复制速度极慢使用字符级复制且缓冲区过小改用块级复制,增大缓冲区到 4KB 以上
目标文件无法删除 / 修改未正确关闭文件(文件被占用)确保fclose被调用,检查是否有异常分支遗漏
第六章:总结与学习建议
  1. 核心流程
    打开文件 → 读取数据 → 写入数据 → 关闭文件,其中 “二进制模式 + 块级复制” 是通用方案。

  2. 必背函数

    • fopen()/fclose():文件生命周期管理。
    • fread()/fwrite():高效读写二进制数据。
    • perror()/ferror():错误处理必备。

形象比喻:把文件复制想象成 “搬砖游戏”

你可以把 “文件复制” 想象成一个超级简单的 “搬砖游戏”:

  1. 两个房子(文件):你有一个 “源文件”(比如装满砖块的老房子)和一个 “目标文件”(空的新房子)。
  2. 卡车(文件指针):你需要开着两辆卡车,分别通向老房子和新房子,告诉电脑 “我要操作这两个文件了”。
  3. 搬砖工(读写函数):卡车司机不能直接搬砖,需要搬砖工帮忙。每次搬一块砖(字符)或者一堆砖(缓冲区),从老房子搬到新房子。
  4. 关门(关闭文件):搬完砖后,一定要记得把两辆卡车开走,关紧房门,否则可能丢砖(数据丢失或占用资源)。
一句话总结流程:

打开源文件(老房子)→ 打开目标文件(新房子)→ 一块一块(或一堆一堆)把数据从源文件搬到目标文件→ 关闭两个文件(关门)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值