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语言-第九期-加餐:文件位置指示器与二进制读写

相关推荐
Heisenberg~1 小时前
详解八大排序(五)------(计数排序,时间复杂度)
c语言·数据结构·排序算法
Elihuss2 小时前
ONVIF协议操作摄像头方法
开发语言·php
lb36363636364 小时前
分享一下arr的意义(c基础)(必看)(牢记)
c语言·知识点
Swift社区5 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht5 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht5 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20245 小时前
Swift 数组
开发语言
南东山人6 小时前
一文说清:C和C++混合编程
c语言·c++
stm 学习ing6 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc7 小时前
《Python基础》之字符串格式化输出
开发语言·python