C 语言文件操作核心

文本文件操作

在 C 语言中创建或打开文件使用的函数是 fopen(),函数原型是:

c 复制代码
FILE* fopen(char const* _FileName, char const* _Mode);

访问模式有以下六种,简单了解一下:

模式 描述
r 只读方式打开一个已有的文本文件。
w 写入方式打开一个文本文件。如果文件不存在,会创建一个新的;如果文件存在,页面的内容会被清空。
a w 不同的是,它会以追加的形式写入文件:如果文件已存在,会在之前的文件内容后追加内容。
r+ 可读可写方式打开一个已有的文本文件。
w+ 可读可写打开文本文件。文件不存在则会创建一个新文件;如果文件存在,会清空文件内容。
a+ w+ 不同的是,读取会从文件的开头开始,写入永远是追加到文件末尾。(执行写入操作时,指针会自动移动到文件的最末尾(越界位置),然后开始写入)

注意: 这里所说的文件指的都是文本文件,二进制文件会在后面提到。

关闭文件使用的是 fclose(),函数原型是 int fclose( FILE *fp );。如果关闭成功返回的是 0,关闭失败返回的是 EOF 常量。

fclose() 的背后会执行这些步骤:

  1. 将缓冲区的数据强制写入磁盘(刷新缓冲区)。
  2. 释放用于该文件的内存与系统资源(如文件描述符)。

文本读写与指针移动示例

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    const char* file_name = "./temp.txt";
    const char* wr_open_mode = "w+";
    FILE* file = fopen(file_name, wr_open_mode);

    // 文件打开失败,退出
    if (file == NULL) {
        return -1;
    }

    // 写入文本
    fputs("Hello World", file);
    fputc('!', file);
    fputc('\n', file);

    // 移动文件位置指针到开头
    // 否则后续的读取会读到文件末尾的空白
    fseek(file, 0, SEEK_SET);

    // 循环读取文件内容
    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), file)) { // 类似的函数有 fgetc,用于读取单个字符
        printf("%s", buffer);
    }

    // 关闭文件资源
    fclose(file);

    return 0;
}

注意: 上述的读取函数在遇到文件结尾符 EOF 时都会停止。其中 fgets 在遇到换行符 \n 时也会停止, 并且它会在读取的字符串尾部补充 \0

其中我们用到了一个移动文件位置指针的函数 fseek(),来实现同一个文件指针能读取刚刚写入的内容,它的函数原型是 int fseek(FILE *stream, long offset, int whence),其中 offset 表示相对基准位置的偏移字节数,where 则是移动的基准点:

基准点 描述
SEEK_SET 文件开头
SEEK_CURRENT 文件当前的位置
SEEK_END 文件末尾

格式化读写函数:fprintf 与 fscanf

类似读写文件的函数还有 fprintffscanf,专门用于写入和读取模板字符串,例如,保存或解析结构化的文本数据,可以这样:

c 复制代码
int main() {
    const char* file_name = "./temp.txt";
    const char* wr_open_mode = "w+";
    FILE* file = fopen(file_name, wr_open_mode);

    if (file == NULL) {
        return -1;
    }

    // 格式化写入
    const char format_str_write[] = "Name: %s, Age: %d\n";
    fprintf(file, format_str_write, "Alice", 20);

    // 读写切换时,刷新缓冲区
    fflush(file);

    // 回到开头
    fseek(file, 0, SEEK_SET);

    // 格式化读取
    const char format_str_read[] = "Name: %[^,], Age: %d\n";
    char name[20];
    int age = 0;
    int ret = fscanf_s(
        file,
        format_str_read,
        name,
        (unsigned int)sizeof(name),
        &age
    );

    if (ret == 2) { // 两个字段都读取成功
        printf("Read from file -> Name: %s, Age: %d\n", name, age);
    }
    else if (ret == EOF) { // 读取完毕
        printf("File read complete\n");
    }
    else { // 格式匹配失败
        printf("File format doesn't match, failed to read\n");
    }

    fclose(file);
    return 0;
}

注意: 读取时,%s 遇到空白符 才会终止,遇到逗号不会停。所以用 %s 会读到 "Alice,",后面模板的 , 对不上,导致匹配失败,所以要换成 %[^,],它遇到逗号就停。

二进制文件操作与文件大小获取

二进制文件的模式与文本文件的模式有点差异,模式需要替换为:"rb", "wb", "ab", "rb+"(等价 "r+b"), "wb+"(等价 "w+b"), "ab+"(等价 "a+b")。

读写二进制文件

处理二进制文件时,我们使用到的核心函数分别是 fread(读)、fwrite(写),以一个图片的复制为例:

c 复制代码
int main() {
    const char* file_name = "./avatar.jpg";
    const char* copy_file_name = "./avatar_copy.jpg";

    FILE* file_r = fopen(file_name, "rb");
    FILE* file_w = fopen(copy_file_name, "wb");

    // 检查打开状态
    if (file_r == NULL || file_w == NULL) {
        if (file_r != NULL) fclose(file_r);
        if (file_w != NULL) fclose(file_w);
        return -1;
    }

    char buffer[1024];
    int buffer_len = sizeof(buffer) / sizeof(buffer[0]);
    int len = 0;

    // 采用块读写的方式,高效复制文件
    while ((len = fread(buffer, sizeof(char), buffer_len, file_r)) != 0) {
        fwrite(buffer, sizeof(char), len, file_w);
    }

    fclose(file_r);
    fclose(file_w);
    return 0;
}

注意: 在 C 语言标准库中没有直接叫字节(byte)的类型,但是我们可以使用标准 1 字节的 charunsigned char 来替换。

获取文件的总大小

获取文件的总大小,可以在上述的遍历代码上进行改造来实现:在不断读取文件内容的同时,累加每次读取到的有效字节数,像这样:

c 复制代码
char buffer[1024];
long total_size = 0;
int len = 0;

while ((len = fread(buffer, sizeof(char), sizeof(buffer), file_r)) != 0) {
    total_size += len; // 累加每次读取的字节数
    fwrite(buffer, sizeof(char), len, file_w);
}
printf("The total size of the file is %ld bytes\n", total_size);

我们也可以通过 fseek + ftell 的组合获取。其中 ftell 可以获取文件位置指针当前相对于文件开头的偏移字节数。

注意:操作二进制文件时,模式一定要使用 b,否则在部分操作系统上指针的定位会受到换行符转换的影响,导致计算出错误的大小。

c 复制代码
int main() {
    const char* file_name = "avatar.jpg";
    FILE* r_file = fopen(file_name, "rb"); // 这里一定要是 rb
    if (r_file == NULL) {
        return -1;
    }

    // 将指针移动到文件末尾
    fseek(r_file, 0, SEEK_END);
    // 获取当前指针位置,即文件总大小
    long file_size = ftell(r_file);

    printf("The total size of the file is %ld bytes\n", file_size);

    fclose(r_file); // 关闭文件
    return 0;
}

大文件的顺序切割

学完了上面的基础后,我们来实现一个进阶功能------文件的顺序切割:将一个大文件拆分为指定数量的子文件。

c 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 获取文件大小的辅助函数
long get_file_size(const char* file_name) {
    FILE* r_file = fopen(file_name, "rb"); // 使用二进制模式
    if (r_file == NULL) {
        return -1L;
    }

    fseek(r_file, 0, SEEK_END);
    long file_size = ftell(r_file);
    fclose(r_file);
    return file_size;
}

const int SPLIT_NUMBER = 3; // 拆分份数
const int MAX_PATH_LEN = 256;

int main() {
    const char* file_name = "avatar.jpg";
    FILE* origin_file = fopen(file_name, "rb");
    if (origin_file == NULL) {
        return -1;
    }

    const char* file_name_placeholder = "avatar_%d.jpg";

    // 计算拆分的每块大小
    long file_size = get_file_size(file_name);
    if (file_size < 0) {
        fclose(origin_file);
        return -1;
    }
    long split_size = file_size / SPLIT_NUMBER;

    // 动态分配二维数组,用于存放每个子文件的文件路径
    char** split_file_names = (char**)malloc(sizeof(char*) * SPLIT_NUMBER);
    for (int i = 0; i < SPLIT_NUMBER; i++) {
        split_file_names[i] = (char*)malloc(sizeof(char) * MAX_PATH_LEN);
        snprintf(split_file_names[i], MAX_PATH_LEN, file_name_placeholder, i); // 生成如 avatar_0.jpg
    }

    // 写入每个子文件
    for (int i = 0; i < SPLIT_NUMBER; i++) {
        FILE* split_file = fopen(split_file_names[i], "wb");
        if (split_file == NULL) {
            continue;
        }

        // 计算当前子文件的字节区间
        long start = i * split_size;
        long end = start + split_size;

        // 如果是最后一块,需要将文件剩余的所有字节都划分进来
        if (i == SPLIT_NUMBER - 1) {
            end = file_size;
        }

        // 准确定位原始文件的读取起点
        fseek(origin_file, start, SEEK_SET);

        // 块读写
        char block_buffer[1024];
        long bytes_to_write = end - start;

        while (bytes_to_write > 0) {
            long read_len = (bytes_to_write > (long)sizeof(block_buffer)) ? (long)sizeof(block_buffer) : bytes_to_write;

            fread(block_buffer, sizeof(char), read_len, origin_file);
            fwrite(block_buffer, sizeof(char), read_len, split_file);

            bytes_to_write -= read_len;
        }

        fclose(split_file);
    }

    // 资源清理
    fclose(origin_file);
    for (int i = 0; i < SPLIT_NUMBER; i++) {
        free(split_file_names[i]);
    }
    free(split_file_names);

    return 0;
}

拆分出的第一张图片(后面两张因为识别不到文件格式,所以无法显示):

相关推荐
鱼儿也有烦恼3 小时前
01.搭建Android Studio开发环境
android·android studio
m0_738120724 小时前
PHP代码审计基础——面向对象(四)
android·开发语言·网络·安全·github·php
Coffeeee5 小时前
搞Android的怎么可能搞不懂Context Engineering?
android·人工智能·ai编程
Carson带你学Android5 小时前
Gemini Nano 开发实战:让你的App轻松拥有端侧 AI 功能
android·ai编程
TDengine (老段)7 小时前
TDengine SMA 索引 — 块级/文件级统计索引
android·大数据·服务器·数据库·人工智能·时序数据库·tdengine
心中有国也有家7 小时前
AtomGit Flutter 鸿蒙客户端: AnimatedScale 与 AnimatedContainer 联合实战
android·javascript·flutter·华为·harmonyos
恋猫de小郭8 小时前
Flutter Windows 开始支持 Impeller ,还修复了多窗口 bug
android·前端·flutter
鱼儿也有烦恼9 小时前
07.RecyclerView、SwipeRefreshLayout、SmartRefreshLayout
android
三少爷的鞋9 小时前
Kotlin Flow 深入解析:`stateIn()` 的真正核心,其实是 SharingStarted
android