文件操作(上):从零开始理解文件读写

说实话,很多初学者学C语言时,最怕的就是文件操作。我当年也一样,总觉得文件这东西看不见摸不着,比指针还玄乎。但后来做了几个实际项目才发现——文件操作其实没那么神秘,说白了就是让程序和磁盘上的数据打交道。

今天咱们先聊文件操作的上半部分:文件的概念、打开关闭、以及最基础的字符和字符串读写。嗯,这部分内容虽然基础,但却是所有文件操作的地基。

1. 文件到底是什么?

在C语言里,文件被抽象成一个字节流。什么意思呢?就是你往文件里写数据,它就是一个接一个的字节排着队;你从文件里读数据,也是一个接一个的字节往外拿。

我个人习惯把文件想象成一条传送带。数据就像箱子,放在传送带上,程序在传送带的一端放箱子(写),或者在另一端取箱子(读)。

C语言把文件分为两种:

  • 文本文件:存的是人能看懂的字符,比如 .txt、.c 源文件。每个字节对应一个字符。
  • 二进制文件:存的是数据的原始二进制形式,比如 .exe、.jpg。你直接打开看是乱码。

你可能会问:这两种文件有啥本质区别?其实在C语言层面,它们都是字节流。唯一的区别在于——文本文件在读写时会做换行符转换。Windows下换行是 \r\n,Linux下是 \n,C的文本模式会自动帮你转换。二进制模式则原样读写,不做任何转换。

核心概念:C语言通过 FILE 结构体来管理文件。这个结构体里保存了文件位置指针、缓冲区、错误标志等信息。你不需要知道它的内部细节,只需要会用 FILE * 这个指针就行。

2. 文件的打开与关闭

操作文件的第一步,就是打开它。C语言用 fopen() 函数来干这件事。

2.1 fopen() 函数

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

第一个参数是文件名,第二个参数是打开模式。返回值是一个 FILE * 指针,如果打开失败则返回 NULL

常用的打开模式有这些:

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

我曾经犯过一个低级错误:用 "w" 模式打开一个重要的配置文件,结果文件内容全被清空了。从那以后,我每次用 "w" 模式都会再三确认——这个文件是不是真的可以覆盖?

2.2 fclose() 函数

文件用完了,一定要关掉。就像借了书要还一样。

int fclose(FILE *stream);

成功返回0,失败返回EOF。为什么一定要关?因为:

  • 释放系统资源(每个进程能打开的文件数量是有限的)
  • 刷新缓冲区,确保数据真正写入磁盘
  • 避免数据丢失(程序崩溃时未关闭的文件可能丢失数据)

我见过不少新手写代码,打开文件后忘了关,结果程序跑着跑着就报 "Too many open files" 的错误。嗯,这个坑我当年也踩过。

2.3 标准写法模板

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

// 读写操作...

if (fclose(fp) != 0) {
    perror("文件关闭失败");
    return -1;
}

这个模板我用了十几年,几乎没变过。你想想看,每次打开文件都检查返回值,每次关闭文件也检查返回值——虽然麻烦,但能避免很多莫名其妙的bug。

3. 文件的读写操作

文件打开后,就可以读写了。咱们先学最基础的四个函数:fgetcfputcfgetsfputs

3.1 fputc() — 写一个字符

int fputc(int ch, FILE *stream);

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

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

char str[] = "Hello, File!";
for (int i = 0; str[i] != '\0'; i++) {
    fputc(str[i], fp);
}

fclose(fp);

这段代码把 "Hello, File!" 一个字符一个字符地写进文件。虽然效率不高,但逻辑很清晰。

3.2 fgetc() — 读一个字符

int fgetc(FILE *stream);

从文件中读取一个字符。成功返回读取的字符(转为int),读到文件末尾或出错时返回 EOF。

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

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

fclose(fp);

注意:这里 ch 必须声明为 int 类型,不能是 char。因为 EOF 的值是 -1,而 char 可能是无符号的(0~255),这样永远无法判断文件结束。我在项目中见过有人用 char 类型,结果读大文件时陷入了死循环——程序一直在读,永远读不到 EOF。

3.3 fputs() — 写一个字符串

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

把字符串 str 写入文件。成功返回非负数,失败返回 EOF。注意:它不会自动添加换行符

FILE *fp = fopen("data.txt", "w");
if (fp == NULL) return -1;

fputs("第一行内容\n", fp);
fputs("第二行内容\n", fp);

fclose(fp);

你看,换行符需要自己加。这和 puts() 不一样,puts() 会自动加换行,但 fputs() 不会。

3.4 fgets() — 读一个字符串

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

从文件中读取一行,最多读取 n-1 个字符,遇到换行符或 EOF 停止。读取的字符串末尾会自动添加 '\0'。

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return -1;

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

fclose(fp);

关键点fgets() 会把换行符也读进来(如果缓冲区够大的话)。所以如果你不想输出换行,可以手动去掉:buffer[strcspn(buffer, "\n")] = '\0';

4. 知识体系总览

说了这么多,咱们用一张图来梳理一下今天的内容:

文件操作(上)知识体系 文件的概念 字节流抽象 文本文件 vs 二进制文件 FILE 结构体 打开与关闭 fopen() 打开模式 fclose() 关闭释放 错误检查(NULL/EOF) 读写操作 fputc() / fgetc() fputs() / fgets() 字符 vs 字符串 核心流程:打开文件 → 检查错误 → 读写操作 → 关闭文件 每一步都要检查返回值,这是文件操作的安全底线

5. 避坑指南与经验总结

最后,分享几个我这些年积累的经验:

  • 每次打开文件都要检查返回值。别偷懒,NULL 检查就是你的护身符。
  • 文件路径要注意转义。Windows 下写路径要用双反斜杠:"C:\\data\\test.txt",或者用正斜杠:"C:/data/test.txt"
  • fgets() 比 gets() 安全一万倍。永远不要用 gets(),它不检查缓冲区长度,是缓冲区溢出的罪魁祸首。
  • 读写完成后立即关闭文件。别等到程序结束才关,万一中间崩溃就麻烦了。

一个小技巧:调试文件操作时,可以在关键位置加 printf 打印文件指针的状态,或者用 feof()ferror() 检查文件流的状态。这样能快速定位问题。

好了,文件操作的上半部分就聊到这儿。这些基础函数虽然简单,但它们是所有高级文件操作(比如 fread/fwrite、fscanf/fprintf)的基石。把今天的内容练熟了,后面的学习就会轻松很多。


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