16、指针与文件操作:文件指针FILE*、fopen/fclose、fread/fwrite、文件读写中的指针偏移

文件操作,说白了就是让程序跟磁盘上的数据打交道。很多C语言初学者觉得文件操作跟指针没啥关系,其实恰恰相反——文件操作的核心就是一个指针,叫FILE*。我当年刚入行时,也以为文件操作就是调几个函数完事,直到有一次项目里读写二进制文件出了大问题,才真正把文件指针这玩意儿搞明白。

16.1 文件指针FILE*——文件操作的“遥控器”

你想想看,你要操作一个文件,总得有个东西代表这个文件吧?FILE*就是干这个的。它是一个结构体指针,里面装着文件的各种状态信息:当前读写位置、缓冲区状态、错误标志等等。

我个人习惯把FILE*想象成一个遥控器——你拿着它,就能控制文件的读写。但注意,它不直接指向文件内容,而是指向一个管理结构。

核心理解:

  • FILE* 是一个指针,指向 FILE 结构体
  • 这个结构体由系统维护,你不需要关心它的内部细节
  • 你只需要通过这个指针调用各种文件操作函数
// 声明一个文件指针
FILE *fp;

// 打开文件,fp指向这个文件的管理结构
fp = fopen("data.bin", "rb");
if (fp == NULL) {
    // 打开失败,处理错误
    perror("文件打开失败");
    return -1;
}

嗯,这里要注意:fopen返回的FILE*绝对不能随便修改。我见过有人试图对fpfp++操作,结果程序直接崩溃。这个指针是系统给你的“凭证”,不是让你随便玩的。

16.2 fopen/fclose——开门和关门

fopenfclose就像一对搭档,一个负责开门,一个负责关门。开门时要告诉系统你要怎么用这个文件——读?写?追加?二进制还是文本?

模式 含义 文件不存在时
"r" 只读(文本) 返回NULL
"w" 只写(文本) 创建新文件
"a" 追加(文本) 创建新文件
"rb" 只读(二进制) 返回NULL
"wb" 只写(二进制) 创建新文件
"ab" 追加(二进制) 创建新文件

我曾经踩过的坑:忘记调用fclose。在嵌入式系统里,文件描述符是有限资源。你打开一个文件不关,系统不会自动帮你回收。有一次我在一个循环里反复打开文件却不关闭,结果跑了几个小时程序就报“Too many open files”。从那以后,我写fopen的同时就会把fclose写好,形成肌肉记忆。

FILE *fp = fopen("config.bin", "rb");
if (fp == NULL) {
    // 错误处理
    return;
}

// ... 读写操作 ...

fclose(fp);  // 别忘了!

16.3 fread/fwrite——二进制数据的搬运工

说到freadfwrite,这两个函数才是真正跟指针深度绑定的地方。它们的原型是这样的:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

注意第一个参数——void*。这意味着你可以传任何类型的指针进去。我个人觉得这是C语言最优雅的设计之一:用一个通用指针,就能读写任意类型的数据。

实用技巧:fread读取结构体时,一定要确保结构体没有对齐问题。我在项目中遇到过结构体里有#pragma pack的情况,读出来的数据全是乱的。建议读写二进制文件时,明确指定结构体的对齐方式。

// 定义一个结构体
typedef struct {
    int id;
    char name[32];
    float score;
} Student;

// 写入一个学生数据
Student stu = {1001, "张三", 95.5f};
fwrite(&stu, sizeof(Student), 1, fp);

// 读取一个学生数据
Student read_stu;
fread(&read_stu, sizeof(Student), 1, fp);

这里有个细节:fread返回的是实际读取的元素个数,不是字节数。如果返回值小于count,说明可能读到文件末尾了,或者发生了错误。我建议每次都检查返回值:

size_t ret = fread(buffer, sizeof(int), 10, fp);
if (ret != 10) {
    if (feof(fp)) {
        printf("读到文件末尾了\n");
    } else if (ferror(fp)) {
        printf("读取出错\n");
    }
}

16.4 文件读写中的指针偏移——fseek/ftell

文件内部其实也有一个“指针”,叫文件位置指针。它指向当前要读写的位置。每次freadfwrite之后,这个指针会自动向后移动。

但有时候,你想跳到文件的某个特定位置去读写。这时候就需要fseekftell了。

// fseek 原型
int fseek(FILE *stream, long offset, int whence);

// whence 取值:
// SEEK_SET - 从文件开头算起
// SEEK_CUR - 从当前位置算起
// SEEK_END - 从文件末尾算起

实际应用场景:我在做嵌入式日志系统时,需要快速定位到日志文件的最后一条记录。如果用fread从头读到尾,效率太低了。我的做法是:

  1. fseek(fp, -sizeof(LogEntry), SEEK_END)跳到倒数第一条记录的位置
  2. fread读取这条记录
  3. ftell获取当前位置,方便后续操作
// 获取文件大小
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);

// 跳回文件开头
fseek(fp, 0, SEEK_SET);

// 跳过前10个int
fseek(fp, 10 * sizeof(int), SEEK_SET);

// 从当前位置后退5个字节
fseek(fp, -5, SEEK_CUR);

我曾经犯过的错:在文本模式下使用fseek。Windows系统下,文本模式会把\n转换成\r\n,导致文件位置指针的计算出现偏差。二进制模式下就没有这个问题。所以,如果你要用fseek做精确定位,一定要用二进制模式打开文件。

16.5 知识体系总览

下面这张图把文件指针操作的核心脉络梳理清楚了。你看一遍,应该就能建立起整体认知。

文件指针操作核心体系 FILE* 文件指针 打开与关闭 fopen() — 打开文件 fclose() — 关闭文件 模式:r/w/a/b 读写操作 fread() — 读取数据 fwrite() — 写入数据 void* 指针传参 指针偏移 fseek() — 定位 ftell() — 获取位置 SEEK_SET/CUR/END ⚠ 常见陷阱 忘记 fclose → 资源泄漏 文本模式下 fseek 定位不准 → 用二进制模式

16.6 综合示例:读取配置文件

最后,我写一个实际项目中常用的例子——读取一个二进制配置文件。这个文件里存着多个结构体,我们需要按索引读取指定的那个。

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

typedef struct {
    int key;
    int value;
} ConfigItem;

ConfigItem* read_config(const char* filename, int index) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        perror("打开文件失败");
        return NULL;
    }
    
    // 计算偏移量:跳到第index个元素的位置
    long offset = index * sizeof(ConfigItem);
    if (fseek(fp, offset, SEEK_SET) != 0) {
        perror("定位失败");
        fclose(fp);
        return NULL;
    }
    
    // 分配内存并读取
    ConfigItem *item = (ConfigItem*)malloc(sizeof(ConfigItem));
    if (item == NULL) {
        fclose(fp);
        return NULL;
    }
    
    size_t ret = fread(item, sizeof(ConfigItem), 1, fp);
    if (ret != 1) {
        free(item);
        fclose(fp);
        return NULL;
    }
    
    fclose(fp);
    return item;
}

int main() {
    ConfigItem *item = read_config("config.bin", 5);
    if (item) {
        printf("key=%d, value=%d\n", item->key, item->value);
        free(item);
    }
    return 0;
}

这个例子把fopenfseekfreadfclose串在了一起。你想想看,如果没有指针偏移,你要读取第5个元素,就得把前面4个都读一遍,多浪费啊。

我的个人习惯:每次写完文件操作代码,我都会检查三件事:

  • 文件打开成功了吗?(检查返回值)
  • 文件关闭了吗?(配对fclose)
  • 读写的数据量对吗?(检查返回值)

这三条检查,帮我避免过无数次线上事故。

文件指针操作,说白了就是管理好三个东西:文件指针的生命周期、数据的搬运、位置的偏移。把这三点吃透了,文件操作这块你就拿下了。


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