14、文件操作(上):文件指针、fopen/fclose、fgetc/fputc、fgets/fputs

文件操作,说白了就是让程序跟硬盘上的文件打交道。你想想看,程序跑在内存里,一断电数据全没了。但文件不一样,它存在硬盘上,关了机再打开,数据还在。这就是文件操作的价值所在。

我个人习惯把文件操作分成三步:打开文件、读写文件、关闭文件。这三步缺一不可,尤其是最后一步,很多人容易忘。嗯,咱们今天就把这三步彻底讲透。

14.1 文件指针——操作文件的“遥控器”

在C语言里,所有文件操作都离不开一个东西——FILE指针。你可以把它理解成一个遥控器,你按哪个键(调用哪个函数),它就帮你操作对应的文件。

FILE是一个结构体类型,定义在里。它里面存了什么?文件位置、缓冲区状态、错误标志……这些你不需要全记住,但你要知道:每个打开的文件,都有一个对应的FILE指针

声明一个文件指针很简单:

FILE *fp;  // fp就是文件指针变量

注意,这时候fp还没指向任何文件,它只是个空壳子。真正让它“上岗”的,是fopen函数。

核心要点:文件指针是文件操作的“身份证”,没有它,寸步难行。

14.2 fopen / fclose——开门与关门

fopen负责打开文件,fclose负责关闭文件。这两个函数必须成对出现,就像你进门要开门,出门要关门一样。

14.2.1 fopen:打开文件

函数原型:

FILE *fopen(const char *filename, const char *mode);

第一个参数是文件名,第二个参数是打开模式。模式决定了你能对文件做什么——读、写、还是追加。

常用的模式有这些:

模式 含义 文件不存在时 文件存在时
"r" 只读 返回NULL 正常打开
"w" 只写 创建新文件 清空内容
"a" 追加 创建新文件 保留内容,在末尾写
"r+" 读写 返回NULL 正常打开
"w+" 读写 创建新文件 清空内容
"a+" 读写追加 创建新文件 保留内容,在末尾写

我在项目中遇到过一个问题:用"w"模式打开一个配置文件,结果文件被清空了。后来才发现,那个文件里存着系统参数,我只是想读一下,却用了写模式。嗯,从此以后,读文件一定用"r",写文件才用"w",这个习惯我一直保持到现在。

代码示例:

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    printf("文件打开失败!\n");
    return -1;
}
// 接下来可以读文件了...

重要:fopen返回NULL时,一定要处理。不要假设文件一定存在。我曾经见过一个程序,没检查返回值就直接用fp读写,结果程序直接崩溃了。

14.2.2 fclose:关闭文件

函数原型:

int fclose(FILE *stream);

关闭文件有两个作用:一是把缓冲区里没写完的数据刷到硬盘上,二是释放系统资源。如果你只fopen不fclose,程序跑久了,系统会告诉你“打开的文件太多了”。

代码示例:

fclose(fp);  // 关闭文件
fp = NULL;   // 好习惯:置空指针

个人技巧:我习惯在fclose之后立刻把fp置为NULL。这样万一后面不小心用了这个指针,程序会立刻崩溃在明处,而不是在某个诡异的地方出bug。

14.3 fgetc / fputc——一个字符一个字符地读写

这两个函数是最基础的文件读写函数。一次只操作一个字符,简单但有效。

14.3.1 fputc:写一个字符

函数原型:

int fputc(int ch, FILE *stream);

把字符ch写到文件里。成功返回写入的字符,失败返回EOF。

代码示例:

FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
    printf("打开文件失败\n");
    return -1;
}

fputc('H', fp);
fputc('e', fp);
fputc('l', fp);
fputc('l', fp);
fputc('o', fp);

fclose(fp);

运行后,output.txt里就写入了"Hello"。

14.3.2 fgetc:读一个字符

函数原型:

int fgetc(FILE *stream);

从文件里读一个字符。成功返回字符的ASCII码,读到文件末尾或出错时返回EOF。

代码示例:

FILE *fp = fopen("output.txt", "r");
if (fp == NULL) {
    printf("打开文件失败\n");
    return -1;
}

int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);  // 输出到屏幕
}

fclose(fp);

这个循环很经典,逐字符读取直到文件结束。注意ch要声明为int而不是char,因为EOF的值是-1,char可能存不下。

避坑指南:我曾经在嵌入式项目里用char类型接收fgetc的返回值,结果文件里有个0xFF的字节,被当成EOF提前结束了循环。排查了半天才发现是类型问题。从此以后,fgetc的返回值一律用int

14.4 fgets / fputs——按行读写,效率更高

逐字符读写太慢了,尤其是处理大文件的时候。fgets和fputs按行操作,效率高得多。

14.4.1 fputs:写一个字符串

函数原型:

int fputs(const char *str, FILE *stream);

把字符串str写入文件。注意,它不会自动加换行符。如果你需要换行,得自己加\n

代码示例:

FILE *fp = fopen("log.txt", "a");
if (fp == NULL) {
    printf("打开文件失败\n");
    return -1;
}

fputs("系统启动成功\n", fp);
fputs("用户登录\n", fp);

fclose(fp);

这里用了"a"模式,每次写入都追加到文件末尾,不会覆盖之前的内容。做日志文件时特别实用。

14.4.2 fgets:读一行字符串

函数原型:

char *fgets(char *str, int n, FILE *stream);

从文件里读一行,最多读n-1个字符,遇到换行符或EOF就停止。读到的内容会存到str里,包括换行符(如果有的话)。

代码示例:

FILE *fp = fopen("log.txt", "r");
if (fp == NULL) {
    printf("打开文件失败\n");
    return -1;
}

char buffer[256];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
    printf("读到一行:%s", buffer);
}

fclose(fp);

注意fgets返回NULL时表示读到文件末尾或出错。这个循环会一直读到文件结束。

实用建议:fgets的第二个参数n一定要给够。如果一行数据超过n-1个字符,fgets会分多次读取,每次读n-1个字符。我一般给256或1024,够用又不浪费内存。

14.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你一看就明白:文件操作就是围绕文件指针展开的,打开、读写、关闭,三步走。

文件操作核心知识体系 文件指针 FILE *fp fopen / fclose 打开与关闭文件 fgetc / fputc 逐字符读写 fgets / fputs 按行读写 ⚠ 常见陷阱 1. 忘记检查 fopen 返回值 → 野指针崩溃 2. 忘记 fclose → 资源泄漏,文件可能没写完 3. fgetc 返回值用 char 接收 → 0xFF 误判为 EOF 三步走:打开 → 读写 → 关闭,缺一不可

14.6 综合示例:文件复制程序

把上面的知识串起来,写一个文件复制程序。从源文件读,写到目标文件里。

#include <stdio.h>

int main() {
    FILE *src, *dst;
    int ch;

    // 打开源文件(只读)
    src = fopen("source.txt", "r");
    if (src == NULL) {
        printf("无法打开源文件\n");
        return -1;
    }

    // 打开目标文件(只写)
    dst = fopen("target.txt", "w");
    if (dst == NULL) {
        printf("无法创建目标文件\n");
        fclose(src);
        return -1;
    }

    // 逐字符复制
    while ((ch = fgetc(src)) != EOF) {
        fputc(ch, dst);
    }

    printf("文件复制完成!\n");

    // 关闭文件
    fclose(src);
    fclose(dst);

    return 0;
}

这个程序虽然简单,但包含了文件操作的全部核心步骤。你可以试试改成用fgets/fputs实现,效率会更高。

总结一下:文件操作没那么神秘。记住三步走——打开、读写、关闭。再记住四个函数——fgetc/fputc、fgets/fputs。剩下的就是多写多练了。


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