传送门:C语言-第八章:指针进阶
目录
[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-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)的关系,文件的二进制操作