第17章 文件操作(一):文件的概念、文件的打开与关闭、文件的读写

各位同学,今天我们来聊聊C语言里一个非常实用的主题——文件操作。说实话,我刚开始学C的时候,觉得文件操作挺神秘的。程序跑完就没了,数据怎么保存下来?后来才明白,文件就是程序和外部世界沟通的桥梁。

我个人习惯把文件操作比作「存取快递」。你要先找到快递柜(打开文件),然后往里面放东西(写文件)或者取东西(读文件),最后记得关好柜门(关闭文件)。这个比喻虽然简单,但核心逻辑就是这么回事。

17.1 文件的概念

文件是什么?说白了,就是存储在外部介质上的一组相关数据的集合。在C语言里,文件被抽象成一个「流」(stream)。你不需要关心数据到底存在硬盘的哪个扇区,只需要通过流来读写数据就行。

C语言把文件分为两类:

  • 文本文件:存储的是字符序列。每个字符对应一个ASCII码。你用记事本打开能看懂的那种。
  • 二进制文件:存储的是数据的二进制形式。比如一个int类型的整数12345,在二进制文件里就是4个字节的原始数据。你用记事本打开会看到乱码。

我记得刚入行时,有个同事把二进制文件当文本文件读,结果数据全乱了。嗯,这里要注意:读写方式必须匹配,否则会出大问题。

核心区别:

  • 文本文件:按字符读写,遇到换行符会做转换(Windows下\r\n转\n)
  • 二进制文件:按字节原样读写,不做任何转换

你想想看,如果你在Linux上写了一个文本文件,拿到Windows上打开,换行符可能就乱了。这就是文本模式带来的麻烦。所以跨平台传输数据时,我建议用二进制模式。

17.2 文件的打开与关闭

在C语言里,操作文件的第一步就是打开它。标准库提供了fopen函数,原型是这样的:

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

它返回一个FILE *指针。如果打开失败,返回NULL。这个指针就是你和文件之间的「遥控器」,所有后续操作都靠它。

第二个参数mode决定了你怎么使用这个文件。我整理了一张表,你一看就明白:

模式 含义 文件不存在时 文件存在时
"r" 只读(文本) 返回NULL 正常打开,位置在开头
"w" 只写(文本) 创建新文件 清空原有内容
"a" 追加(文本) 创建新文件 位置在末尾,保留原有内容
"rb" 只读(二进制) 返回NULL 正常打开
"wb" 只写(二进制) 创建新文件 清空原有内容
"ab" 追加(二进制) 创建新文件 位置在末尾
"r+" 读写(文本) 返回NULL 正常打开,位置在开头
"w+" 读写(文本) 创建新文件 清空原有内容
"a+" 读写追加(文本) 创建新文件 位置在末尾

打开文件后,一定要记得关闭。用fclose函数:

int fclose(FILE *stream);

关闭成功返回0,失败返回EOF。为什么要关闭?因为每个进程能打开的文件数量是有限的。我曾经在项目里见过一个bug:程序跑了一整天后突然打不开文件了,排查了半天才发现是文件打开后没关闭,文件描述符泄漏了。嗯,这种错误很隐蔽,但后果很严重。

避坑指南: 每次fopen后,一定要检查返回值是否为NULL。我曾经见过新手直接拿NULL指针去读写,程序直接崩溃。养成习惯:

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

17.3 文件的读写——字符级操作

文件打开后,就可以开始读写了。先从最基础的字符级操作说起。

17.3.1 fgetc:读一个字符

int fgetc(FILE *stream);

这个函数从文件中读取一个字符。返回的是int类型,不是char。为什么?因为除了返回字符的ASCII码,它还要返回EOF(-1)来表示文件结束。如果用char,-1和0xFF就分不清了。

来看个例子:

#include <stdio.h>

int main() {
    FILE *fp = fopen("hello.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    fclose(fp);
    return 0;
}

这段代码逐字符读取文件内容并打印到屏幕上。注意循环条件:(ch = fgetc(fp)) != EOF。这个写法很经典,赋值和判断一步完成。

17.3.2 fputc:写一个字符

int fputc(int c, FILE *stream);

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

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

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

fclose(fp);

这段代码会在output.txt里写入"Hello"。很简单,对吧?

小技巧: 如果你要写入一个字符串,别用fputc一个个写,太慢了。后面会介绍fputsfprintf,效率高得多。

17.4 文件的读写——字符串级操作

字符级操作虽然灵活,但效率不高。实际项目中,我们更常用字符串级的读写函数。

17.4.1 fgets:读一行字符串

char *fgets(char *s, int size, FILE *stream);

这个函数从文件中读取一行,最多读取size - 1个字符。遇到换行符或EOF就停止。读取的内容会存入s指向的缓冲区,并在末尾自动添加\0

成功返回s,失败或读到文件末尾返回NULL

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }

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

    fclose(fp);
    return 0;
}

这里有个细节:fgets会保留换行符。如果一行内容超过size - 1个字符,fgets只读取前size - 1个,剩下的留在下一次读取。所以缓冲区大小要合理设置,我一般设成256或1024。

避坑指南: 我曾经在项目里看到有人用gets函数。千万别用!gets不检查缓冲区大小,很容易造成缓冲区溢出。标准库已经把它标记为废弃了。永远用fgets代替gets

17.4.2 fputs:写一行字符串

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

把字符串s写入文件。注意:fputs不会自动添加换行符。你需要自己加\n

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

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

fclose(fp);

这段代码以追加模式打开文件,写入两行日志。每次运行都会在文件末尾追加内容,不会覆盖之前的记录。这在写日志文件时非常实用。

17.5 知识体系总览

说了这么多,我们来画一张图,把本章的知识点串起来。这样你脑子里会有一个清晰的框架。

文件操作知识体系 文件操作 打开与关闭 fopen() fclose() 模式选择 字符级读写 fgetc() fputc() 字符串级读写 fgets() fputs() 核心原则:打开→检查→读写→关闭 文本模式 vs 二进制模式 要区分

17.6 综合示例:文件复制工具

最后,我们来写一个完整的例子。把source.txt的内容复制到dest.txt。这个程序综合运用了本章的所有知识点。

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

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

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

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

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

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

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

    return 0;
}

这个程序虽然简单,但包含了文件操作的所有关键步骤:打开、检查、读写、关闭。你想想看,如果中间漏了任何一个检查,程序都可能出问题。

个人建议: 在实际项目中,我习惯把文件操作封装成函数。比如open_file_safe()read_line_safe(),这样错误处理逻辑可以复用,代码也更清晰。

好了,这一章的内容就到这里。文件操作是C语言里非常实用的技能,几乎每个项目都会用到。多写几个例子,你就能熟练掌握。


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