【C语言-文件操作】2. 文件的读写——字符、字符串、格式化、二进制

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 5
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

上节课咱们把文件打开了,也学会了怎么关闭。但光打开不干活,那不是白开了吗?这节课咱们就讲怎么往文件里写东西,怎么从文件里读东西。

C语言提供了四种读写方式,从简单到复杂,从“一个字一个字来”到“一整块一整块搬”。咱们挨个说,每种都配上例子,最后你就能根据实际需要挑着用了。

方式一:一个字符一个字符地读写——fgetc和fputc

这是最基础的读写方式。你一次处理一个字符,简单粗暴,但适合处理文本文件。

fputc——往文件里写一个字符

int fputc(int ch, FILE *fp);

两个参数:ch是要写的字符,fp是文件指针。成功返回写的那个字符,失败返回EOF。

用法:

FILE *fp = fopen("test.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);

运行完,文件里就是“Hello”五个字符。

fgetc——从文件里读一个字符

int fgetc(FILE *fp);

从文件里读一个字符,返回读到的字符(转成int了)。如果读到文件末尾,返回EOF。EOF是stdio.h里定义的一个常量,通常是-1。

用法:

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

int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);   // 把读到的字符打印到屏幕上
}

fclose(fp);

这个循环是文件读取的经典套路——读一个字符,判断是不是EOF,不是就处理,是就退出。

注意:fgetc返回的是int,不是char。 因为EOF是-1,char可能是无符号的(0到255),存不下-1。所以必须用int来接。

完整例子:文件复制

把源文件的内容一个字符一个字符地复制到目标文件:

#include <stdio.h>

int main() {
    FILE *src = fopen("source.txt", "r");
    FILE *dest = fopen("dest.txt", "w");
    int ch;

    if (src == NULL || dest == NULL) {
        perror("打开文件失败");
        return 1;
    }

    while ((ch = fgetc(src)) != EOF) {
        fputc(ch, dest);
    }

    fclose(src);
    fclose(dest);

    printf("复制完成\n");
    return 0;
}

这个例子很简单,但你想想——这不就是copy命令的原理吗?读一个字符,写一个字符,循环往复,直到读完。

fgetc/fputc适合什么场景?

  • 处理文本文件,逐字符分析

  • 简单的文件复制

  • 字符级别的过滤(比如把文件里所有小写字母转成大写)

方式二:一行一行地读写——fgets和fputs

一个字符一个字符处理太慢了,尤其是文本文件,你经常是按行处理的。fgets和fputs就是干这个的。

fputs——往文件里写一个字符串

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

把str写到文件里,不会自动加换行符。成功返回非负数,失败返回EOF。

FILE *fp = fopen("test.txt", "w");
fputs("第一行\n", fp);   // 要换行得自己加\n
fputs("第二行\n", fp);
fputs("第三行", fp);     // 最后一行不加\n也行
fclose(fp);

fgets——从文件里读一行

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

三个参数:str是接收字符串的缓冲区,n是最多读多少个字符(包括最后的\0),fp是文件指针。成功返回str,失败或读到文件末尾返回NULL。

char buffer[100];
FILE *fp = fopen("test.txt", "r");

while (fgets(buffer, sizeof(buffer), fp) != NULL) {
    printf("%s", buffer);   // 打印读到的这一行
}

fclose(fp);

注意:fgets会把换行符也读进来。 比如文件里有一行是“Hello\n”,fgets读到的就是“Hello\n”,不是“Hello”。如果你不想要那个换行符,得自己手动去掉:

buffer[strcspn(buffer, "\n")] = '\0';   // 把换行符替换成\0

strcspn函数返回字符串中第一个匹配字符的位置,这里就是找\n的位置。如果没找到,返回字符串长度,那这个操作就相当于在末尾加了个\0,没影响。

fgets最多读n-1个字符,因为要留一个位置给\0。如果一行特别长,超过n-1个字符,fgets只读前n-1个,剩下的下次再读。

完整例子:按行读取文件并打印行号

#include <stdio.h>
#include <string.h>

int main() {
    FILE *fp = fopen("test.txt", "r");
    char buffer[256];
    int line = 1;

    if (fp == NULL) {
        perror("打开失败");
        return 1;
    }

    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        // 去掉末尾的换行符
        buffer[strcspn(buffer, "\n")] = '\0';
        printf("%d: %s\n", line, buffer);
        line++;
    }

    fclose(fp);
    return 0;
}

fgets/fputs适合什么场景?

  • 处理配置文件、日志文件,按行读取

  • 从文件读取字符串数据

  • 写简单的文本报告

方式三:格式化读写——fprintf和fscanf

你平时用printf往屏幕上打印格式化的数据,用scanf从键盘读格式化的数据。文件也有对应的版本:fprintf和fscanf。用法跟printf和scanf一模一样,就是多了一个文件指针参数。

fprintf——往文件里写格式化数据

int fprintf(FILE *fp, const char *format, ...);
FILE *fp = fopen("students.txt", "w");
fprintf(fp, "姓名:%s,学号:%d,成绩:%.1f\n", "张三", 101, 85.5);
fprintf(fp, "姓名:%s,学号:%d,成绩:%.1f\n", "李四", 102, 92.0);
fclose(fp);

文件里就会有两行格式化的学生信息。

fscanf——从文件里读格式化数据

int fscanf(FILE *fp, const char *format, ...);
FILE *fp = fopen("students.txt", "r");
char name[20];
int id;
float score;

while (fscanf(fp, "姓名:%s,学号:%d,成绩:%f\n", name, &id, &score) == 3) {
    printf("%s %d %.1f\n", name, id, score);
}
fclose(fp);

fscanf返回成功匹配并赋值的参数个数。上面这个例子中,如果成功读到三个数据,返回3。当返回的不是3(比如读完了,返回EOF),循环退出。

注意:fscanf的格式字符串必须跟文件里的内容格式严格匹配。 文件里是“姓名:张三,学号:101,成绩:85.5”,你的格式字符串就得是"姓名:%s,学号:%d,成绩:%f"。差一个字符都读不出来。所以fscanf比较“脆”,文件格式一变,读取代码就得跟着改。

完整例子:把学生信息存到文件再读出来

#include <stdio.h>

int main() {
    // 写入
    FILE *fp = fopen("students.txt", "w");
    if (fp == NULL) { perror("打开失败"); return 1; }

    fprintf(fp, "%s %d %.1f\n", "张三", 101, 85.5);
    fprintf(fp, "%s %d %.1f\n", "李四", 102, 92.0);
    fprintf(fp, "%s %d %.1f\n", "王五", 103, 78.5);
    fclose(fp);

    // 读取
    fp = fopen("students.txt", "r");
    if (fp == NULL) { perror("打开失败"); return 1; }

    char name[20];
    int id;
    float score;

    printf("学生信息:\n");
    while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
        printf("%s %d %.1f\n", name, id, score);
    }
    fclose(fp);

    return 0;
}

fprintf/fscanf适合什么场景?

  • 需要按特定格式读写数据

  • 把结构化的数据存成文本文件

  • 调试时往文件里打印日志

方式四:二进制块读写——fread和fwrite

前面三种都是针对文本文件的。如果你要存的是结构体、数组这种二进制数据,用文本方式存又慢又占空间,而且读回来还得解析。这时候就该fread和fwrite上场了。

fwrite——把一块内存的数据原封不动写到文件里

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

四个参数:

  • ptr:要写的数据在内存里的地址

  • size:每个元素占多少字节

  • count:要写多少个元素

  • fp:文件指针

返回值:成功写入的元素个数。正常情况等于count。

int arr[5] = {1, 2, 3, 4, 5};
FILE *fp = fopen("data.bin", "wb");   // 注意模式是"wb",二进制写
fwrite(arr, sizeof(int), 5, fp);
fclose(fp);

这行代码把整个数组的5个int原封不动地写到了文件里,总共写了5 * sizeof(int)个字节。

fread——从文件里把一块数据原封不动读回内存

size_t fread(void *ptr, size_t size, size_t count, FILE *fp);

参数跟fwrite对应,返回值是实际读到的元素个数。如果读完了文件,返回值可能小于count。

int arr[5];
FILE *fp = fopen("data.bin", "rb");   // 二进制读
int n = fread(arr, sizeof(int), 5, fp);
printf("读了%d个元素\n", n);
fclose(fp);

注意:用二进制模式读写的时候,fopen的模式要加b——"wb"、"rb"、"ab"。在Windows上这个区别很重要,因为Windows会把\n转成\r\n。在Linux上b可加可不加,效果一样。但为了跨平台,建议加上。

完整例子:结构体的二进制存取

#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main() {
    Student s1 = {101, "张三", 85.5};
    Student s2 = {102, "李四", 92.0};

    // 写入
    FILE *fp = fopen("students.dat", "wb");
    if (fp == NULL) { perror("打开失败"); return 1; }

    fwrite(&s1, sizeof(Student), 1, fp);
    fwrite(&s2, sizeof(Student), 1, fp);
    fclose(fp);

    // 读取
    fp = fopen("students.dat", "rb");
    if (fp == NULL) { perror("打开失败"); return 1; }

    Student s;
    while (fread(&s, sizeof(Student), 1, fp) == 1) {
        printf("%d %s %.1f\n", s.id, s.name, s.score);
    }
    fclose(fp);

    return 0;
}

你打开students.dat看看,里面是一堆乱码——因为它是二进制文件,不是给人看的,是给程序读的。但它存取效率高,而且结构体里有什么它就存什么,读回来也是原样。

fread/fwrite适合什么场景?

  • 存结构体、数组等二进制数据

  • 需要快速读写大量数据

  • 数据不需要给人直接看

  • 游戏存档、数据库文件、图片音频

四种方式对比——什么时候用哪个?

 
方式 函数 适用场景 优点 缺点
字符读写 fgetc/fputc 逐字符处理文本 简单、精细控制 慢,代码啰嗦
字符串读写 fgets/fputs 按行处理文本 方便,一次一行 有换行符问题
格式化读写 fprintf/fscanf 结构化文本数据 格式灵活 文件格式变了读取就失败
二进制块读写 fread/fwrite 结构体、数组、二进制数据 快,省空间,精度不丢 人看不懂,跨平台可能有字节序问题

给你一个简单的选择标准:

  • 数据是给人看的(配置文件、日志、报告)→ 用文本方式(fgets/fputs或fprintf/fscanf)

  • 数据是给程序用的(存档、缓存、数据库)→ 用二进制方式(fread/fwrite)

  • 你要精细处理每个字符 → 用fgetc/fputc

几个常见的坑

坑1:fgetc返回类型写成char

char ch;
while ((ch = fgetc(fp)) != EOF) { ... }   // 可能死循环

因为char可能是无符号的,永远不等于-1。必须用int。

坑2:fgets忘了处理换行符

fgets(buffer, 100, fp);
printf("%s\n", buffer);   // 会多一个空行,因为buffer里已经有\n了

坑3:fscanf格式字符串跟文件内容不匹配

// 文件里是:101 张三 85.5
fscanf(fp, "%d %s %f", &id, name, &score);   // 正确
fscanf(fp, "%d,%s,%f", &id, name, &score);   // 错误!文件里没逗号

坑4:用文本模式打开二进制文件

FILE *fp = fopen("data.bin", "r");    // 错误!应该用"rb"
FILE *fp = fopen("data.bin", "rb");   // 正确

坑5:fread的返回值不检查

fread(&s, sizeof(Student), 1, fp);
printf("%s\n", s.name);   // 如果读失败,s里是垃圾数据

要检查返回值,确定真的读到了数据再用。

坑6:fwrite写字符串时多写了\0

char str[] = "Hello";
fwrite(str, sizeof(char), strlen(str), fp);   // 正确,只写5个字符
fwrite(str, sizeof(char), sizeof(str), fp);   // 会多写一个\0

总结一下:

这节课讲了四种读写方式,核心就这些:

  • fgetc/fputc:一个字符一个字符来,适合精细处理文本。

  • fgets/fputs:一行一行来,适合处理日志、配置。

  • fprintf/fscanf:格式化读写,适合结构化文本。

  • fread/fwrite:二进制块读写,适合结构体、数组,快且省空间。

不管用哪种方式,核心流程都是一样的:打开文件→读写→关闭文件。你根据数据的类型和用途选合适的方式就行。

回去自己动手,每种方式都写个例子跑一遍。把学生信息分别用文本方式和二进制方式存一遍,然后用记事本打开看看区别。亲手试过了,你才知道什么时候该用哪种。下节课咱们讲文件定位——怎么在文件里跳来跳去,想读哪儿读哪儿。