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

相关推荐
半条-咸鱼5 小时前
C语言基础语法+STM32实践学习笔记 | 指针/寄存器核心应用
c语言·stm32·学习·嵌入式
hzb666665 小时前
xd_day47文件上传-day55xss
javascript·学习·安全·web安全·php
WYH2875 小时前
TTSY-学习笔记1
笔记·学习
鄭郑6 小时前
【Playwright 学习笔记 03】CSS选择器 定位方法
css·笔记·学习·playwright
JeffDingAI6 小时前
【Datawhale学习笔记】参数高效微调
android·笔记·学习
zhangrelay6 小时前
笔记本电脑待机功耗最低能降到多少瓦特?占用资源极少的系统有哪些呢?
笔记·学习
凸头6 小时前
Nginx配置学习
运维·学习·nginx
木风小助理7 小时前
未来JS架构:Realm隔离——从全局共享到独立环境的必然
学习
非凡ghost7 小时前
批量校正图像方向(校正PDF页面方向)
windows·学习·pdf·软件需求