C 语言-文件操作学习

在编程时,你是否遇到过这样的困扰:程序运行时计算出的数据,关掉程序就不见了?下次想继续用这些数据,只能重新运行程序重新计算?其实解决这个问题很简单,核心就是学会 "文件操作"------ 把数据存到硬盘上,需要时再读出来。

一、先搞懂:为什么需要文件?

我们写程序时,数据默认存在内存 里。但内存有个特点:程序退出后,内存就会被系统回收,里面的数据也会跟着消失(比如你用计算器算完结果,关掉计算器就找不到之前的数字了)。

文件 是存在硬盘上的,属于 "持久化存储"------ 哪怕电脑关机,数据也不会丢。所以文件操作的核心目的就是:让程序的数据能长期保存,下次运行时直接使用。

二、什么是文件?C 语言里的文件分两种

平时我们说的 "文件",比如电脑里的文档、图片,都是硬盘上的文件。但在 C 语言中,从功能角度主要分为两类:

  1. 程序文件 :就是我们写的代码相关文件,比如后缀为.c的源文件、.obj的目标文件、.exe的可执行文件(这些是程序运行的 "基础",不是我们要操作的数据);
  2. 数据文件 :专门用来存储程序运行时需要读写的数据的文件(比如程序计算的结果、需要读取的配置信息),这也是我们今天重点讨论的对象。

文件名的小知识

一个文件要有唯一的 "身份证"------ 文件名,它由 3 部分组成:文件路径 + 文件名主干 + 文件后缀 。比如c:\code\test.txt

  • 路径c:\code(文件存在哪个文件夹里);
  • 主干test(文件的名字);
  • 后缀.txt(文件类型,文本文件)。

三、文本文件 vs 二进制文件:数据怎么存?

根据数据的存储形式,数据文件又分为两种,核心区别在于 "是否转换数据格式":

  1. 二进制文件 :数据在内存中是二进制形式存储的,直接输出到硬盘,不做任何转换。优点是存储高效(占空间小),缺点是肉眼看不懂(打开是乱码);

(obj文件是二进制方式存储,用文本编辑器是乱码)

  1. 文本文件数据存储前会转换成 ASCII 码形式。优点是肉眼能直接看懂(比如打开是 "1234"),缺点是占空间更大。

(.c文件是文本文件)

举个直观的例子:存储整数10000

  • 文本文件:每个数字是一个 ASCII 字符,10000共 5 个字符,占 5 个字节;
  • 二进制文件:直接存10000的二进制值,只占 4 个字节(效率更高)。

四、关键概念:流和文件指针

在学文件操作前,先记住两个核心概念,否则后面的函数会看不懂:

1. 什么是 "流"?

程序要向硬盘写数据、从硬盘读数据,中间需要一个 "桥梁"------ 因为不同设备(键盘、显示器、硬盘)的输入输出规则不一样,为了方便程序员操作,C 语言抽象出了 "流" 的概念。

你可以把 "流" 想象成一条 "字符河":程序写数据就是往河里 "放水",读数据就是从河里 "取水"。不管是操作键盘、显示器还是文件,都通过 "流" 来统一处理,不用关心底层设备的差异。

2. 标准流:程序启动时默认打开的 3 个 "河"

为什么我们用scanf能直接从键盘输入,用printf能直接在屏幕输出?因为 C 语言程序启动时,会默认打开 3 个标准流:

  • stdin:标准输入流(对应键盘),scanf就是从这里读数据;
  • stdout:标准输出流(对应显示器),printf就是往这里写数据;
  • stderr:标准错误流(也对应显示器),用来输出程序的错误信息。

这 3 个流默认打开,所以我们不用手动操作就能用scanfprintf

3. 文件指针:操作文件的 "工具手"

要操作文件,我们需要一个 "工具" 来管理文件的信息(比如文件名、文件当前的读写位置、文件状态)------ 这就是**FILE*类型的文件指针**。

可以这么理解:每个打开的文件,系统都会在内存中创建一个 "文件信息区 "(类似文件的 "说明书"),而文件指针就是指向这个 "说明书" 的指针。通过这个指针,我们就能间接操作对应的文件。

定义文件指针很简单:FILE* pf;(pf 就是一个文件指针变量)。

五、核心操作:文件的打开和关闭

就像我们用记事本一样:用之前要先打开,用完要关闭。C 语言操作文件也遵循这个逻辑,而且必须养成 "打开就关闭" 的习惯(否则可能导致数据丢失、文件损坏)。

1. 打开文件:用 fopen 函数

函数原型:FILE * fopen (const char * filename, const char * mode);

  • 第一个参数:文件名(比如"test.txt",如果文件不在当前文件夹,要写全路径,比如"c:\code\test.txt");
  • 第二个参数:打开模式(关键!决定了文件是只读、只写还是读写,是文本文件还是二进制文件);
  • 返回值:打开成功返回文件指针(指向文件信息区),打开失败返回NULL(比如文件不存在却要 "只读" 打开)。

例子(要打开的的文件不存在):

cpp 复制代码
#include<stdio.h>
int main()
{
	FILE *fp=fopen("test.txt","r");
	if (fp == NULL)
	{
		perror("Error opening file");
		return 1;
	}



	return 0;
}

例子(以只读方式创建打开一个文件):

cpp 复制代码
#include<stdio.h>
int main()
{
	FILE *fp=fopen("test.txt","w");
	if (fp == NULL)
	{
		perror("Error opening file");
		return 1;
	}

	fclose(fp);
	fp = NULL;

	return 0;
}

程序运行后,打开创建的文件,向其中随便输入文字,在运行这个程序会把里面的内容清空。

再次运行后:

2. 关闭文件:用 fclose 函数

函数原型:int fclose (FILE * stream);

  • 参数:要关闭的文件指针(比如之前定义的pf);
  • 注意:关闭后要把文件指针设为NULL(比如pf = NULL;),避免变成 "野指针"(指向无效地址,导致程序出错)。

常用的文件打开模式(重点记这几个)

打开模式 含义 文件不存在时
"r" 只读(文本文件) 出错(打开失败)
"w" 只写(文本文件) 新建一个文件
"a" 追加(文本文件,往文件末尾写) 新建一个文件
"rb" 只读(二进制文件) 出错
"wb" 只写(二进制文件) 新建一个文件
"r+" 读写(文本文件) 出错
"w+" 读写(文本文件) 新建一个文件

六、文件的读写操作:顺序读写和随机读写

打开文件后,核心就是 "读"(从文件拿数据到程序)和 "写"(把程序数据存到文件)。C 语言提供了一套专门的函数,分为 "顺序读写" 和 "随机读写" 两类。

1. 顺序读写:按文件内容的顺序 "从头读到尾"

顺序读写就是从文件开头开始,读 / 写完一个数据后,指针自动移到下一个位置,不能跳着来。常用函数如下(记熟这些就够日常用了):

写一个字符:

cpp 复制代码
#include<stdio.h>
int main()
{
	FILE *fp=fopen("test.txt","w");
	if (fp == NULL)
	{
		perror("Error opening file");
		return 1;
	}

	//写文件
	fputc('a', fp);//(往 pFile 指向的文件写字符 'a');
	fputc('b', fp);
	fputc('c', fp);

	fclose(fp);
	fp = NULL;

	return 0;
}

运行后查看文件:

循环写26个字符:

读一个字符:

cpp 复制代码
#include<stdio.h>
int main()
{
    
    FILE* p = fopen("test.txt", "r");
    if (p == NULL)
    {
        perror("Error opening file");

        return 1;
    }

    //// 依次读取前三个字符并打印
    //char a = fgetc(p);
    //printf("%c", a);

    //char b = fgetc(p);
    //printf("%c", b);

    //char c = fgetc(p);
    //printf("%c", c);
    int ch = 0;
    while ((ch = fgetc(p)) != EOF)
    {
        printf("%c ",ch);
    }

    fclose(p);
    p = NULL;

    return 0;
}

例子2:(fscanf,fprintf的使用)

cpp 复制代码
#include<stdio.h>

struct S
{
    char name[20];
    int age;
    float score;
};

int main()
{
    struct S s = {"djsj", 33, 99.9};
    // "w+"模式:可读写的文本模式,文件不存在则创建,存在则清空
    FILE *pf = fopen("test.txt", "w+");
    if (pf == NULL)
    {
        perror("Error opening file");
        return 1;
    }

    // 写文件:格式化写入结构体数据
    int write_ret = fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);
    if (write_ret < 0) { // fprintf返回写入的字符数,失败返回负数
        perror("Error writing to file");
        fclose(pf);
        return 1;
    }

    // 关键:将文件指针重置到文件开头,否则读不到内容
    fseek(pf, 0, SEEK_SET);

    // 读文件:格式化读取到新的结构体变量(避免覆盖原数据更易验证)
    struct S s_read = {0}; // 初始化读取用的结构体
    int read_ret = fscanf(pf, "%s %d %f", s_read.name, &s_read.age, &s_read.score);
    if (read_ret != 3) { // fscanf返回成功读取的变量个数,这里应读取3个
        perror("Error reading from file");
        fclose(pf);
        return 1;
    }

    // 打印读取的内容(方式1)
    printf("方式1:%s %d %.1f\n", s_read.name, s_read.age, s_read.score);
    // 打印读取的内容(方式2:stdout就是屏幕,效果和printf一致)
    fprintf(stdout, "方式2:%s %d %.1f\n", s_read.name, s_read.age, s_read.score);

    // 关闭文件,避免资源泄漏
    fclose(pf);
    pf = NULL;
    return 0;
}

2. 随机读写:想读哪里就读哪里

有时候我们不想按顺序来,比如想直接修改文件中间的内容,这就需要 "随机读写"------ 通过调整文件指针的位置来实现。核心函数有 3 个:

(1)fseek:移动文件指针

函数原型:int fseek (FILE * stream, long int offset, int origin);

  • 作用:根据 "起始位置" 和 "偏移量",把文件指针移到指定位置;
  • 参数说明:
    • origin:起始位置(3 个可选值):
      • SEEK_SET:文件开头;
      • SEEK_CUR:文件指针当前位置;
      • SEEK_END:文件末尾;
    • offset:偏移量(正数往后移,负数往前移)。

例子:

cpp 复制代码
#include<stdio.h>
int main()
{
    
    FILE* p = fopen("test.txt", "w");
    if (p == NULL)
    {
        perror("Error opening file");

        return 1;
    }
    fputs("This is an apple.", p);
    //fseek(p, 9, SEEK_SET);// 从文件开头(SEEK_SET)往后移9个位置(第10个字符)
    //fputs(" sam", p);//  修改后文件内容变成"This is sam apple."
    fclose(p);
    p = NULL;

    return 0;
}

去掉代码的注释再运行的结果:

例子(fread于fwrite):

cpp 复制代码
#include<stdio.h>
int main()
{
	int  arr[] = {1,2,3,4,5};
	int arr1[] = { 0 };

	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	//写数据
	int sz=sizeof(arr)/sizeof(arr[0]);
	fwrite(arr, sizeof(arr[0]), sz, pf);//把arr的内容以二进制的形式写入文件

	// 将文件指针重置到文件开头
	fseek(pf, 0, SEEK_SET);

	//读数据
	fread(arr1, sizeof(arr[0]), sz, pf);//把文件内容以二进制的形式读入arr
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	fclose(pf);
	pf = NULL;

	return 0;
}
(2)ftell:获取指针偏移量

函数原型:long int ftell (FILE * stream);

  • 作用:返回文件指针相对于 "文件开头" 的偏移量(比如指针在第 10 个字符,返回 9,因为从 0 开始计数);
  • 常用场景:计算文件大小(把指针移到文件末尾,获取偏移量就是文件字节数)。
(3)rewind:指针回到文件开头

函数原型:void rewind (FILE * stream);

  • 作用:直接把文件指针移回文件开头(比如读了一半文件,想重新读,就用这个函数)。

七、避坑重点:文件读取结束怎么判断?

很多新手会用feof函数来判断文件是否读完,这是错误的!

正确的判断逻辑:

feof的作用不是 "判断文件是否结束",而是 "判断文件结束的原因"------ 是读到文件尾了,还是读的时候出错了(比如文件损坏)。

正确的判断方法分两种:

  1. 文本文件 :判断读写函数的返回值:
    • fgetc:读失败或到文件尾,返回EOF
    • fgets:到文件尾,返回NULL
  2. 二进制文件 :判断fread的返回值是否小于 "实际要读的个数"(比如想读 5 个数据,返回 3,说明只读到 3 个,文件结束了)。
cpp 复制代码
FILE* fp = fopen("test.txt", "r");
if (!fp) {
    printf("文件打开失败");
    return 1;
}
int c; // 注意用int,因为EOF是-1,char存不下
// 循环读每个字符,直到返回EOF
while ((c = fgetc(fp)) != EOF) {
    putchar(c); // 输出到屏幕
}
// 读完后判断原因
if (ferror(fp)) {
    printf("读取文件出错");
} else if (feof(fp)) {
    printf("文件正常读完");
}
fclose(fp);
fp = NULL;

八、文件缓冲区:为什么写了数据,文件里看不到?

有时候我们用函数写了数据到文件,但打开文件却发现没有内容,这是因为 C 语言有 "文件缓冲区" 的机制。

什么是缓冲区?

系统会在内存中为每个打开的文件开辟一块 "临时存储区"------ 写数据时,不会直接写到硬盘,而是先存到缓冲区;只有当缓冲区满了、调用fflush函数刷新,或者调用fclose关闭文件时,缓冲区的数据才会真正写到硬盘上。

关键结论:

  1. 操作文件后,一定要fclose关闭文件(关闭时会自动刷新缓冲区);
  2. 如果不想等缓冲区满,也可以用fflush(pf)手动刷新(注意:高版本 VS 可能不支持);
  3. 不关闭文件也不刷新缓冲区,可能导致数据丢失(比如程序异常退出,缓冲区的数据没写到硬盘)。

最后:文件操作的核心步骤总结

不管是读还是写文件,都遵循以下 4 个步骤,按这个来就不会出错:

  1. 打开文件(fopen)→ 检查是否打开成功;
  2. 进行读写操作(顺序读写或随机读写);
  3. 关闭文件(fclose)→ 指针置空(避免野指针);
  4. (可选)判断读写结果(比如是否读完、是否出错)。

C 语言文件操作看似知识点多,但核心逻辑很简单:就是 "打开 - 操作 - 关闭" 的流程,再记住几个常用函数和避坑点,就能轻松实现数据的持久化存储。

相关推荐
西岸行者3 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意3 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码3 天前
嵌入式学习路线
学习
毛小茛3 天前
计算机系统概论——校验码
学习
babe小鑫3 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms3 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下3 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。3 天前
2026.2.25监控学习
学习
im_AMBER3 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J3 天前
从“Hello World“ 开始 C++
c语言·c++·学习