🐖前言
🐕1.为什们我们要用文件
在我们之前写程序时,如果使用scanf
函数用键盘输入数据,这些东西都放到内存当中,一旦退出程序,那么这些数据就会消失,比如就像我们写的通讯录,不管是静态的还是动态的,每次打开程序,都要重新开始录入人的信息。
⭐那有什么方法把通讯录的信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
🐕2 什么是文件
磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
🐶2.1程序文件
包括源程序文件(后缀为.c),头文件(后缀.h),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
🐶2.2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据。
比如程序运行需要从中读取数据的文件,或者输出内容的文件。
像在使用通讯录的时候,我们可以将人的信息存放在文件中,通过程序的操作,能够将其读到内存,或者通过程序的操作,可以在文件中录入信息
⭐本章重点就是讨论数据文件
🐶3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt 为了方便起见,文件标识常被称为文件名。
🐕3. 文件的打开和关闭
🐶3.1文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。
该结构体类型是有系统声明的,取名FILE.
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
⭐所以我们只需要了解,这是个什么东西,并且他能干什么就好了。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:FILE* pf;
//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
🐶3.2 文件的打开和关闭
是一个文件,那么就涉及到文件的打开和文件的关闭,那么怎么打开或者关闭文件呢,这时就涉及到两个函数,文件打开函数
fopen
,文件关闭函数fclose
,通过我们仅学的英语,我们就可以知道为什们这样写他的函数名。
⭐下面介绍一下这两个函数怎么用。
🦊3.2.1fopen
的使用
如果同学们自学能力强,就可以自己在这个网站上自学这个函数。
-
const char * filename, const char * mode
第一个参数打开的文件名,而第二个参数是文件的打开方式。 - 对于文件来说,打开方式有很多种请看下图和下表,这些都是他的打开方式,每种方式打开的效果肯定都不相同,所以下面讲述文件的使用的时候会仔细讲解。现在只要知道大概是干什么就行了。
fopen
的参数
fopen
的第二个参数的打开方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
-
FILE *
,返回一个文件指针。
文件打开成功,指向所要打开的文件。文件打开失败,指向空指针。
fopen
的返回值
🦊3.2.2fclose
的使用
-
FILE * stream
,他的参数就是所要关闭文件的文件指针。 -
int
,返回值是一个整型,如果文件成功关闭,则返回零值。失败时,将返回 EOF。
但是我们需要注意的一点是,为了避免生成野指针,再关闭完文件后,需要将指针设为空指针,可以类比一下动态内存函数free
,在释放完空间,需要将指针设为空指针。
fclose
的参数
fclose
的返回值
🦊3.2.3代码实现
int main()
{
//打开文件
FILE* pf = fopen("text.txt", "w");
//防止打不开文件返回空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//这时就可以写文件了
//关闭文件
fclose(pf);
pf = NULL;
//防止空指针,生成野指针
return 0;
}
这是有同学有疑问了,文件名不是文件路径吗,怎么上面没有文件路径呢,只有文件名主干+文件后缀 呢,其实这是相对路径,他在写这个代码文件下创建文件。
那么有相对路径,就会有绝对路径,对于绝对路径,就是将文件路径也写出来,最后就在你写的那个文件路径下创建,这就是他的区别。
🐕4文件的读写
对于文件的读写来说,文件的读写分为顺序读写和随机读写,这两块都是很重要的,下面就一一讲解。
🐶4.1文件的顺序读写
首先,对于文件的读写,里面有一些函数,先看下表在作解释。
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
看玩这个表,大家可能感觉还是有点懵,不要怕,先用我们会的知识类比一下我们就知道了,我们常用的
scanf
和printf
函数,他们是怎么用的呢。
scanf
和printf
函数都是通过外部工具来改变内存,
⭐scanf
是通过键盘将数据输入内存。(读操作)
⭐printf
是内存通过屏幕将数据输出。(写操作)
所谓的键盘和屏幕就是外部设备,而我们现在要了解的就是通过文件来该改变内存。
首先我们先了解一下什么是文件的输入和输出函数,
⭐所谓文件输入函数,就是通过文件将数据读入内存。(读操作)
⭐文件输出函数,就是内存将数据写在文件中。(写操作)
好了我们现在了解了这些东西,我们就可以看着表格
🍔文件输入函数,内存读文件,
如果一个字符一个字符的读,就可以用fgetc
函数
如果是一行一行的读,就可以用fgets
函数
如果格式化的数据输入,就用fscanf
函数
如果是二进制的数据输入,就用fread
函数
🍔文件输出函数,内存写在文件上
如果一个字符一个字符的写,就可以用fputc
函数
如果是一行一行的写,就可以用fputs
函数
如果格式化的数据输出,就用fprintf
函数
如果是二进制的数据输出,就用fwrite
函数
🦊4.1.1输出字符函数fputc
-
字符输出函数,可以将所要的字符打印到想要的文件中。但是注意的一点就是他适用于所有输出流,具体下面解释
-
int character, FILE * stream
第一个参数就是所要写的字符,而第二个参数就是所要输出的文件指针。 -
int
如果输出成功,返回该字符,输出失败就返回EOF
。 - 代码如下
fputc
的作用
fputc
的参数
fputc
的返回值
fputc
怎么代码实现
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//防止是空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//使用fputc写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🦊4.1.2读取字符函数fgetc
很容易看出来,这个跟上面这个函数是个对立面。
-
FILE * stream
参数是文件指针,就是要从那个文件获取字符。 -
int
输入成功,就返回该字符,返回失败,就返回EOF - 代码如下
fgetc
的参数
fgetc
的返回值
fgetc
怎么用呢
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
//防止是空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//使用fgetc读文件
int ret = fgetc(pf);
printf("%c ", ret);
ret = fgetc(pf);
printf("%c ", ret);
ret = fgetc(pf);
printf("%c ", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们是将fputc
的内容用fgetc
打印出来,ret
变量我们并没有改变他,但是我们三次打印的结果都不一样,说明什么,说明在用输入函数后文件指针会自己加1,跳到下一个字符。
⭐但是大家发现一个问题没有,我们在打印的时候我们是知道他里面有三个字符我们才用的3,但是在我们平常使用的时候我们并不知道文件里有多少个字符,这时我们就要用到他的返回值,当他返回失败的时候就会返回EOF
,我们可以利用这个特点使用这个函数。那就看下面这个代码。
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
//防止是空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//使用fgetc读文件
int ret = 0;
while ((ret = fgetc(pf)) != EOF)
{
printf("%c ", ret);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🦊4.1.3输出行函数fputs
可以类比一下,
fputs
是输出字符,而fputs
则是输出一行。
-
const char * str, FILE * stream
,第一个参数所要传的字符串地址,第二个参数就是文件指针。 -
int
,输出成功,返回非负值,输出失败,返回EOF。 - 代码如下
fputs
的参数
fputs
的返回值
fouts
的实现代码
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//防止是空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//使用fputs写文件
fputs("hello", pf);
//我们知道hello字符串就表示首元素地址
fputs("1024 Happy", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在文件中就有这样想要达到换行的效果,在代码末尾加入
\n
.效果图就是第二张图片,我们下面的读取文本行就用这个文件。
🦊4.1.4读取文本行函数fgets
-
char * str, int num, FILE * stream
⭐第一个参数是读取完字符串要拷贝到的字符串指针,
⭐第二个参数就是拷贝的最大字符数,包括空字符(\0
),这个有点说法,一会看代码,代码解释。
⭐第三个参数要读取文件的文件指针。 -
char*
,读取成功后,该函数返回str
。失败返回EOF
- 代码如下
fgets
函数的参数
fgets
函数的返回值
fgets
函数的代码实现
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
//防止是空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//使用fgets读文件
char str[] = "##########";
fgets(str, 5, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们这个图是调试后,
str
的内容,我们要读取的是5个字符,比实际的字符少一个字符,因为其中包括\0
,所以要读取"hello"
字符串,就要读取6个字符
但是我们第一行只有"hello"
这个字符串,只能读取这个字符,就算多读取字符,那也不能读取,他读到\n
就停下来了。具体效果图看下图。如果想读下一行就再引用一次就好了。
🦊4.15格式化输出函数fprintf
我们上面将字符,文本行的读写都介绍了一遍怎么使用,但是当我们使用结构体到文件中我们怎么用呢
这时我们就需要用到我们的格式化输出函数
⭐ 那么这个函数怎么用呢,我们认识printf
,那我们就将他们对比一下,发现好像很相似。
-
FILE * stream, const char * format, ...
,第一个参数文件指针,就是输出到那个文件中,第二个参数发现和printf
一样,其实就和他是相同的。 -
int
,输出成功后,将返回写入的字符总数。如果发生写入错误,则设置错误指示器(ferror)并返回负数。
fprintf
的参数
fprintf
的返回值
struct stu
{
char name[20];
int age;
float score;
};
int main()
{
//创建一个结构体变量
struct stu s = { "tongtong",20,100.0 };
//打开文件
FILE* pf = fopen("test.txt", "w");
//判断是否为空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//输入格式化结构体
fprintf(pf, "%s %d %.1f ", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们看一看文件里有什么,大家就知道他怎么用了
🦊4.1.6格式化输入函数fscanf
一个输入,一个输出,大同小异,就不多赘述了
-
FILE * stream, const char * format, ...
第一个参数文件指针,就是输出到那个文件中,第二个参数发现和scanf
一样,其实就和他是相同的。 -
int
,成功后,该函数返回成功填充的参数列表的项数。此计数可以匹配预期的项目数,也可以由于匹配失败、读取错误或文件末尾的范围而减少(甚至为零)。
如果在读取时发生读取错误或到达文件末尾,则会设置正确的指示器(feof 或 ferror)。而且,如果在成功读取任何数据之前发生任何一种情况,则返回 EOF。
fscanf
的参数
fscanf
的返回值
struct stu
{
char name[20];
int age;
float score;
};
int main()
{
//创建一个结构体变量
struct stu s = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断是否为空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//输入格式化结构体
fscanf(pf, "%s %d %.1f ", s.name, &(s.age), &(s.score));
printf("%s %d %.1f ", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输入了一个结构体
🦊4.1.7二进制输出函数fwrite
-
const void * ptr, size_t size, size_t count, FILE * stream
,第一个参数是指向要写入的元素数组的指针,转换为常量 void*。第二个参数要写入的每个元素的大小(以字节为单位)。size_t是无符号整数类型。第三个参数元素数。第四个参数指向指定输出流的 FILE 对象的指针。 - 返回成功写入的元素总数。如果此数字与 count 参数不同,则写入错误会阻止函数完成。在这种情况下,将为流设置误差指示器(ferror)。如果大小或计数为零,则该函数返回零,并且错误指示器保持不变。
fwrite
的参数
fwrite
的返回值
struct stu
{
char name[20];
int age;
float score;
};
int main()
{
//创建一个结构体变量
struct stu s = { "tongtong",20,100.0 };
//打开文件
FILE* pf = fopen("test.txt", "w");
//判断是否为空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//输入格式化结构体
fwrite(&s,sizeof(struct stu),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们这时看一眼文件,发现我们看不懂,这只有计算机能够看懂,他是二进制
🦊4.1.8二进制输入函数fread
-
void * ptr, size_t size, size_t count, FILE * stream
,这个跟写函数的参数一样。就是读,变成了写 -
返回成功读取的元素总数。
如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都设置了正确的指示器,可以分别使用铁道和feof进行检查。
如果大小或计数为零,则该函数返回零,并且 ptr 所指向的流状态和内容保持不变。
fread
的参数
fread
的返回值
struct stu
{
char name[20];
int age;
float score;
};
int main()
{
//创建一个结构体变量
struct stu s = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "rb");
//判断是否为空指针
if (pf == NULL)
{
perror("fopen");
return 1;
}
//输入格式化结构体
fread(&s, sizeof(struct stu), 1, pf);
printf("%s %d %.1f ", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
最后将文件中的内容也打印出来了,文件中的内容是用
fwrite
写的
🐖小总结
对于
printf
,scanf
他们所作用的外部设备分别是屏幕和键盘,而这个就是标准输出输入流,
而在上面那个表格我们可以看到fputc
,fgetc
,fputs
,fgets
,fprintf
,fscanf
,这些适用于所有输出输入流,二进制的输入输出函数这些只能作用于文件。
什么是所有输出输入流?难道这些也能作用到键盘和屏幕吗,答案是可以的,这时就跟printf
,scanf
相同了,而这些标准输出输入流,不像文件需要自己打开,自己关闭,都是系统默认打开的。
⭐对于任何一个C程序,只要运行起来
就默认打开3个流
stdin
-----标准输入流—键盘
stdout
—标准输出流—屏幕
stderr
—标准错误流—屏幕
⭐他们三个的类型都是FILE*
int main()
{
int ch = fgetc(stdin);
fputc(ch, stdout);
return 0;
}
输入一个q,在输出一个q,其实可以这样比较一下
scanf
=return 0;
printf
=fputc(ch, stdout);
🐶5.文件的随机读写
上面我们介绍了文件顺序读写所要用到的函数,还有用文件操作的知识写的通讯录,我们就可以模仿学习上面的函数学习随机读写的函数。
🦊5.1.1fseek
-
FILE * stream, long int offset, int origin
第一个参数:文件指针,指向所要读写的文件
第二个参数:偏移量,所指向的字符的偏移量。
第三个参数:从那里开始,对于偏移量的参考,有三种方式,请看下表 -
int
,如果成功,该函数将返回零。否则,它将返回非零值。
如果发生读取或写入错误,则设置错误指示器(ferror)。
fseek
的参数
fseek
的返回值
第三个参数 | 参考位置 |
---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件结尾 * |