C语言-第九章:文件读写

传送门:C语言-第八章:指针进阶

目录

第零节:准备工作

第一节:文件的简单介绍

1-1.绝对路径与相对路径

1-2.文件的作用

第二节:文件操作

2-1.打开文件

2-2.关闭文件

2-3.读操作

[2-3-1.fgetc 读取一个字符](#2-3-1.fgetc 读取一个字符)

[2-3-2.fgets 读取一行内容](#2-3-2.fgets 读取一行内容)

[2-3-3.fscanf 格式化读取数据](#2-3-3.fscanf 格式化读取数据)

2-4.写操作

[2-4-1.fputc 写入一个字符](#2-4-1.fputc 写入一个字符)

文件缓冲区

[2-4-2.fputs 写入一个字符串](#2-4-2.fputs 写入一个字符串)

[2-4-3.fprintf 格式化写入数据](#2-4-3.fprintf 格式化写入数据)

下期预告:


第零节:准备工作

在学习文件的读写,我们先要把电脑中显示完整文件名(包括后缀)的功能打开,否则看到的文件名是不完整的:

然后在新的项目里创建一个名为"text.txt"的文本文件,接下来我们将操作这个文件:

第一节:文件的简单介绍

1-1.绝对路径与相对路径

我们在C语言中操作的文件和平时使用来记录文字的文件是同一个东西,平时在电脑上打开文件时,我们可能需要点进各种文件夹中,这个过程就是根据文件的路径找到文件,文件路径在windows的窗口中也有显示:

它的路径就是"此电脑 \ Windows-SSD(C:) \ Windows \ addins",这种从最外面到最里面的路径叫做绝对路径

还有一种路径叫做相对路径,比如图片中我在 "addins" 文件夹下,它里面有一个文件:

我就可以用 "./ FXSEXT.ecf" 找到这个文件,其中 "." 代表当前文件。

在C语言中,我们就是通过绝对路径 或者相对路径找到文件的。

1-2.文件的作用

想想我们之前为什么使用文件,我们使用文件就是为了将信息记录下来,方便以后的阅读。

信息记录就需要向文件里写入数据,这就是文件的写操作 ;阅读就需要从文件读出数据,这就是文件的读操作

在对文件进行读写之前,我们需要先打开文件 ,使用完文件后又需要关闭文件

接下来将逐一介绍上述的文件操作。

第二节:文件操作

2-1.打开文件

打开文件需要用到 fopen 函数,所需头文件是<stdio.h>,函数原型如下:

filename 表示文件名,包括文件的路径+文件主干+文件后缀;mode表示文件的打开方式;如果打开文件成功就会返回一个 FILE 类型的结构体指针(文件指针),指向的结构体存放了文件的信息,否则返回NULL。

文件的打开方式见下图,目前只看 r、w、a 方式即可:

还记得之前我们创建的文件 "text.txt" 吗,我们现在以读方式打开它:

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

int main()
{
	FILE* pf = fopen(".\text.txt", "r"); // 相对路径 
	if (pf == NULL)
	{
		perror("文件打开失败\n");
		return 0;
	}
	printf("文件打开成功\n");
	return 0;
}

文件居然打开失败了,错误原因是fopen被传入了一个非法的参数,它的问题出在".\text.txt"中,这是因为在C语言中 '\' 不是单纯的右斜杠,而是转义字符,比如 换行符 '\n'、字符串的结束标志 '\0'

'\' 会和它的下一个字符组成另一个字符,就是这个字符的意思被改变了,上述".\text.txt"中的"\t" 就变成了另外一种字符。

那么如何在C语言中表示 '\' 呢?我们给 '\' 转义即可,即 '\\' 表示单右斜杠,所以在C语言中表示 '\' 要用双斜杠:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r"); // 相对路径 
	if (pf == NULL)
	{
		perror("文件打开失败\n");
		return 0;
	}
	printf("文件打开成功\n");
	return 0;
}

用绝对路径打开文件时也要注意 '\' 的转义作用:

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

int main()
{
	FILE* pf = fopen("C:\\Users\\lyc53\\Desktop\\C语言代码\\C语言测试\\C语言测试\\text.txt", "r"); // 绝对路径,注意'\'的转义作用 
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	printf("文件打开成功\n");
	return 0;
}

2-2.关闭文件

当对文件的操作结束时就可以关闭文件了,文件在打开它的程序结束时会关闭,我们也可以调用 fclose 函数关闭它,函数原型如下:

stream 就是文件指针,fclose 会关闭它,关闭文件后就不能对文件进行任何的读写操作了。

2-3.读操作

文件里要有数据才能读出数据来,首先我们手动给文件加入一些数据:

注意:在文件种存储的数据都是以字符的形式存储的,比如上述的 2024 就是字符串"2024",而不是整数2024

然后我们用读方式 (r) 打开,文件:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r"); // 读方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	return 0;
}

读文件有许多方式,我们一个一个的介绍:

2-3-1.fgetc 读取一个字符

fgetc用于从文件中读取一个字符,函数原型如下:

stream 就是要从哪个文件中读取,读取成功返回这个字符的ascll码值,读取失败返回文件结束符EOF(它实际上是-1,由define定义),以下是一个使用方法:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char c;
	c = fgetc(pf);
	printf("%c\n",c);
	return 0;
}

确实读到了字符 '2',那如果我再次调用,它还会读到字符 '2' 吗?请看以下案例:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char c,d;
	c = fgetc(pf); // 第一次读取 
	d = fgetc(pf); // 第二次读取
	printf("%c\n", c);
	printf("%c\n", d);
	return 0;
}

我们会发现它读取到了文件里的第二个字符。

明明调用一样的函数,为什么结构不同呢?这是因为文件指针里有一个文件位置指示器,它刚开始是指向文件开头的,但是只要每读取1字节的数据,指针就会向后偏移1位。这个指针我们可以用其他函数任意调整它的位置,这个以后会说。

接下来我们一直提取文件的数据,直到全部读完:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char c;
	while (EOF != (c = fgetc(pf))) // 读到EOF说明是文件末尾了,不需要再读了
	{
		printf("%c", c);
	}
	return 0;
}

2-3-2.fgets 读取一行内容

除了一个一个的读取,我们还可以一次读取多个,fgets 函数的原型如下:

str 就是接收读取内容的字符串,num 是读取个数,stream是被读取的文件,返回值是 str 的首元素地址,读取失败返回NULL,它的具体用法如下:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char str[20];
	fgets(str, 4, pf); // 读取4个字符
	printf("%s\n", str);
	return 0;
}

传入的读取个数是4,为什么只得到三个字符呢?这是因为C语言的字符串需要 '\0' 作为结束标志,它也要占用一个位置,所以实际读取的个数是 3

当读到EOF时,即使读取的字符数不足,也不会继续读取了:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char str[20];
	fgets(str, 20, pf); // 读取4个字符
	printf("%s\n", str);
	return 0;
}

文件里的字符数是12(空格也是一个字符),传入的读取数是19(加上一个'\0'),但是也不会读取一些奇奇怪怪的数据到 str 中,因为读到文件末尾读取就停止了。

2-3-3.fscanf 格式化读取数据

我们之前常用的 scanf 函数就是从键盘读取数据到指定位置,而且可以用 %d、%c等格式化参数指定读取的数据类型,而 fscanf 是从文件读取数据到指定位置,它也可以使用格式化参数指定读取的数据类型,它的函数原型如下:

stream 就是要读取的文件;format 就是读取的数据类型,. . .名为可变参数,即参数的数量是不定的,就像一个 scanf 可以同时改变一个变量或两个变量的值一样;函数的返回值是一个整数,返回读取到的字符数,这个函数的具体用法如下:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char str[20];
	fscanf(pf,"%s",str); // 提取一个字符串
	printf("%s\n",str);
	return 0;
}

我们发现它并没有读完所有内容,而是读到空格就结束了,这是这个函数的特性,它将空格换行符作为两个独立数据的分隔符,所以对于需要读取的数据,在存储时也要用空格分开。

我们再来看看这个函数读取其他类型的数据的情况:

(1)读取整型

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	int num;
	fscanf(pf,"%d",&num); // 提取一个整型
	printf("%d\n",num);
	return 0;
}

我们发现它只会读取数字字符,当读取到非数字字符时,它就不读取了,然后把所有读到的数字字符转成整型。

(2)读取字符

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "r");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	char c;
	fscanf(pf,"%c",&c); // 提取一个字符串
	printf("%c\n",c);
	return 0;
}

我们发现它会把文件中的2作为字符看待,所以只读取了一个字符 '2'。

所以对于相同的文件内容,不同的读取方式决定了函数对读取的态度。

2-4.写操作

进行写文件之前我们先把文件中的内容清空。

我们以写方式(w)打开文件:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "w"); // 写方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	return 0;
}

2-4-1.fputc 写入一个字符

fputc 对标的是 fgetc,都是操作一个字符,函数原型如下:

character:要写入的字符

stream:被写入的文件

返回值:如果写入成功,返回被写入字符;如果写入失败,返回EOF

它的具体用法如下:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "w");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	fputc('a',pf); // 写入'a'
	return 0;
}

执行完程序后我们打开文件,字符 'a' 就成功被写入了:

那么如果我们把写入的字符换成 'b' 再次运行,文件的内容会是 ''ab'' 吗,请看执行结果:

结果只有最新写入的 'b' ,这是因为以写方式(w)打开文件时,fopen 函数会清空文件内容,接下来我们再次以w方式打开文件,但是不写入任何内容,就会发现文件内容被清空了:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "w");
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	return 0;
}

如果我们不想清除文件内容,而是对文件内容进行追加,我们可以用追加方式(a)打开文件,它除了可以写入数据,还会把文件位置指示器指向文件末尾:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "a"); // 以追加的方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	fputc('a',pf); // 写入a
	return 0;
}

多次运行上述代码,文件的内容是:

一共4个 'a' 说明上述代码运行了四次。

现在我们清空文件内容,作下一个测试。

文件缓冲区

我们以调试的方式运行文件,运行完 fputc 函数时就停止:

fputc 函数以及执行完了,那么 'a' 应该已经写入文件了,但是此时文件内容仍然为空:

这是为什么呢?这是因为在程序与文件之间还有一个文件缓冲区,程序所有的写入操作都是写入到文件缓冲区中,然后由文件缓冲区真正写入到文件中。

那么文件缓冲区什么时候向文件中写入数据呢?

(1)文件关闭时:

程序结束时文件会关闭,也可以调用 fclose 函数让文件关闭:

此时文件关闭了,我们看看文件里是否有内容了:

不出所料,因为文件关闭了,字符 'a' 就被写到文件中了。

(2) fflush 函数

fflush 函数的作用是强制文件缓冲区向文件写入数据,又叫冲刷缓冲区,函数原型如下:

stream:要冲刷的文件的缓冲区

返回值:返回0表示冲刷成功;返回EOF表示冲刷失败

它的用法如下:

此时文件没有关闭,但是 fflush 函数以及调用完毕了, 此时文件已经有了写入的内容:

那么为什么要有文件缓冲区呢?

提高程序与文件的IO速度:实际上无论读写都要用到文件缓冲区,因为文件的位置在磁盘,距离CPU较远,交互慢。如果每次读取/写入数据都要访问一次文件就太慢了,所以把数据打包好(放入缓冲区),一次性全部处理好那么就只需要访问一次了,效率就大大提高了。

2-4-2.fputs 写入一个字符串

fputs 对标的是 fgets ,但是不能控制写入的字符个数,读取到 '\0' 时就结束写入,函数原型如下:

str:要写入的数据 (字符串)

stream:写入的文件

返回值:成功返回一个非负值;失败返回EOF

它的具体用法如下:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "w"); // 以写方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char str[] = "Hello world";
	fputs(str, pf); // 写入字符串
	return 0;
}

2-4-3.fprintf 格式化写入数据

fprintf 对标 fscanf ,它可以将不同的数据组合成一个字符串写入文件中,函数原型如下:

stream:被写入的文件

format: 写入的数据类型

. . .:可变参数,,不解释

返回值:成功则返回写入的字符总数;失败就返回一个负数

它的用法如下:

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

int main()
{
	FILE* pf = fopen(".\\text.txt", "w"); // 以写方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	char name[] = "Eric";
	char ID[] = "202244567";
	int age = 18;
	fprintf(pf,"%s %s %d",name,ID,age); // 写入多个信息
	return 0;
}

文件的内容是:

我们还可以使用这个函数将结构体的数据存储在文件中:

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

struct student
{
	char name[20];
	char ID[20];
	int age;
};

int main()
{
	FILE* pf = fopen(".\\text.txt", "w"); // 以写方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	// 学生信息
	struct student Jack = { "Jack","202212345",17 };
	struct student Bob = { "Bob","202212347",18 };
	struct student Steve = {"Steve","202212348",19};
	fprintf(pf, "%s %s %d\n", Jack.name, Jack.ID, Jack.age);
	fprintf(pf, "%s %s %d\n", Bob.name, Bob.ID, Bob.age);
	fprintf(pf,"%s %s %d\n", Steve.name, Steve.ID, Steve.age);
	return 0;
}

下期预告:

下一次讲述的是文件位置指示器的操作,键盘、屏幕与文件(scanf 与 fscanf,printf 与 fprintf)的关系,文件的二进制操作

传送门:C语言-第九期-加餐:文件位置指示器与二进制读写

相关推荐
海清河晏111几秒前
Qt实战:从零构建美化登录界面
开发语言·c++·qt
一只小灿灿10 分钟前
C++ 各类特殊符号、运算符
开发语言·c++
Fu_Lin_14 分钟前
《Qt嵌入式从零基础到精通》前言与阅读指南
开发语言·qt
名字还没想好☜17 分钟前
Go 的 select 实战:超时、非阻塞收发与优雅退出的三个套路
开发语言·数据库·golang·go·并发
geovindu18 分钟前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
Scott9999HH9 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海10 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖11 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎