目录 (点击传送 🐱🏍)
创作不易,多多支持一下,🙏
如有不对之处,恳请读者批评指正
一、什么是文件
1、文件的概念
对于文件相比大家都不陌生,这里我们给出比较权威一点的定义:
文件是以硬盘为载体存储在计算机上的信息集合
文件系统功能是操作系统的重要功能和组成部分。在Windows操作系统中打开“我的电脑”,或“资源管理器”,可以看到很多文件,每个文件都有自己的文件名和自己的属性。
在操作系统中,文件是指驻留在外部介质(如磁盘等)中的一个有序数据集,可以是源文件、目标程序文件、可执行文件,也可以是待输入的原始数据,或者是一组输出的结果。使用应用程序的时候,通常保存功能实现把数据从内存中写入文件中,这就是所谓的“存盘”,打开功能实现把磁盘文件的内容读取到内存。
就比如我们平时所写的记事本,都是先把数据写入到内存当中去,当保存后,才会把这些数据从内存写入到磁盘文件中。。
2、文件类型
从程序设计的角度来说,一般分为两种:程序文件 和 数据文件 (从文件的不同功能来分类)
程序文件一般有 程序源文件(后缀为.c)
目标文件(windows环境后缀为.obj)
可执行程序(Windows环境为.exe)
数据文件 内容不一定为程序,而是程序运行时所需要的数据
3、文件名
4、文本文件和二进制文件
在c语言中,根据数据存储的编码形式,数据文件可以分为文本文件和二进制文件两种。
文本文件是以字符ASCLL码值进行存储与编码的文件,其文件内容就是字符。那么二进制自然就是存储二进制文件。从文件的逻辑结构上讲,c语言把文件看作是数据流,也就是后面讲的
FILE* stream,并将数据按顺序以一维方式组织存储。
5、缓冲文件系统
由于系统对磁盘文件数据的存取速度与内存数据存取访问速度不同,而文件数据量较大,数据从磁盘读取到内存或从内存写到磁盘不可能瞬间完成,所以为了提高效率,c程序对文件的处理采用缓冲文件系统的方式进行,程序与文件的数据交换通过文件缓冲区来完成。
二、为什么要使用文件
想必我们都学过结构体,在写结构体的时候我们应该都写过用于存储数据的线性表——链表,就比如说我们再熟悉不过的通讯录。但是我们在使用的时候可能会发问,不是说链表是用来存储数据的吗,为什么当我输入数据后,退出程序的时候数据又都不存在了,等到下次再运行的时候又要重新输入?
这就要谈到文件的重要性了,我们之前所写入程序的数据都是存放在内存当中,当程序退出的时候,内存当中的数据自然就全部销毁了。如果我们能把输入到程序里面的数据给存起来,等到下次用的时候再加载到程序当中,这就做到了数据的持久化,上面的问题也就迎刃而解。
我们使用文件,将数据直接存放到文件当中,然后下次要用的时候只需要重新加载就可以了
三、文件打开和关闭
1、文件指针
struct _iobuf {char * _ptr ;int _cnt ;char * _base ;int _flag ;int _file ;int _charbuf ;int _bufsiz ;char * _tmpfname ;};typedef struct _iobuf FILE ;
FILE * pf ; // 文件指针变量
这个pf指向某个文件的信息区(一个结构体变量),然后通过这个信息区来访问这个文件。
2、文件的打开和关闭
前面所说到,知道了文件的信息区就可以访问文件,那么在访问之前应该先打开文件,当不再使用文件的时候就应该关闭文件。
对应打开文件,ANSIC规定使用fopen函数来打开文件:其定义如下
FILE * fopen ( const char * filename , const char * mode );返回类型为FILE*filename为文件名,类型为char*mode为操作方式,类型为char*
int fclose ( FILE * stream );返回类型为int类型stream为文件流,后面会讲到
操作方式(mode)具体有如下:
文件操作方式 | 含义 | 指定文件不存在 |
"r"(只读) | 为输入数据,打开一个已经存在的文件 | 直接报错 |
"w"(只写) | 为输入数据,打开一个已经存在的文本文件 | 建立一个新的文本文件 |
"a"(追加) | 像文本文件尾添加数据 | 建立一个新文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 直接报错 |
"wb"(只写) | 为输出数据,打开一个二进制文件 | 建立一个新的文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
"rb+"(读写) | 为了读写,打开一个二进制文件 | 报错 |
"wb+"(读写) | 为了读写,建立一个新的二进制文件 | 建立一个新的文件 |
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf = fopen("ilike.txt", "w");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
fputs("abcdefg", pf);
fclost(pf);
pf=NULL
}
return 0;
}
我们首先看到我们工程文件的目录里面:显示时不存在ilike.txt这个文件
当我们运行代码后,里面出现了ilike.txt这个文件:
打开ilikex.txt这个文件显示正如我们代码所输入的那样:
四、顺序读写
我们已经知到该如何打开和关闭我们要访问的文件,但是,如何对文件内部用代码进行操作呢?
接下来就要谈到顺序读写函数
功能 | 函数名 | 用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
1、fputc
作用:将字符写入文件
返回值为字符ascll码值,类型为int,int character 为字符的ascll码值,stream为文件流
- 返回值为所写入字符的ascll码值。
- 每次写入都会让位置指示器所指向的字符将向后移一位。
- 如果写入错误,将会返回EOF,并为文件指示器设置ferror。
例如,我们要对一个文本文件进行写入26个小写字母的操作:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a = 0;
FILE* pf = fopen("ilike.txt", "w"); //打开文件
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
for (int i = 0;i < 26;i++)
{
a=fputc('a'+i, pf);//文件写入操作
printf("%d ", a);
}
fclose(pf);//关闭文件
pf = NULL;//将pf滞空,防止野指针
}
system("pause");
return 0;
}
打开项目目录查看ilike.txt的文件查看,结果如下:
2、fgetc
既然能将字符写入文件,那么自然也能从文件读取字符进行输出。我们查阅c++reference可以看到
解释说明:
- int fgetc (FILE * stream); 返回值为访问的文件的当前位置指示符指向的字符的ascll码值,参数为文件流。
- 每次读取后,当前位置指示符指向的字符会指向下一个字符。
- 如果在调用的时候遇到文件尾,则返回EOF(值为-1).
- 如果读取错误,则返回EOF并为流设置错误指示符FERROR。
读的时候,打开方式用"r",我们读取上面用fputc写入的26个小写字母并打印出来,举个例子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int arr[26] = { 0 };
FILE* pf = fopen("ilike.txt", "r");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
for (int i = 0;i < 26;i++)
{
arr[i] = fgetc(pf);
}
fclose(pf);
pf = NULL;
}
for (int i = 0;i < 26;i++)
{
printf("%c ", arr[i]);
}
system("pause");
return 0;
}
运行结果如图
如果对文件的操作每次只能读取或者写入一个字符,那么效率会不会有点低下了?
这就要说说下面两个函数了:fgets,fputs。
3、fputs
首先想要使用这个函数,就必然要看看这个函数是如何定义的,同样我们查阅c++reference:
解释一下:
- 作用:将一个字符串写入文件流。(str是要写入的字符串,stream为文件流)
- 如果写入成功,则返回一个非负的值,如果写入失败则返回EOF,并未该文件设置错误的指示器。
#include<stdio.h>
#include<stdlib.h>
int main()
{
char arr[10] = { 0 }; //定义要写入的字符串
int a = 0;
for (int i = 0;i < 9;i++)
{
arr[i] = 'x';
}
FILE* pf = fopen("ilike.txt", "w"); //打开文件
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
a=fputs(arr, pf); //写入文件
fclose(pf); // 关闭文件
pf = NULL; //指针置空
}
printf("%d", a);
system("pause");
return 0;
}
打开工程目录里面的ilike.txt文件,结果如下
(我们用一个整形接收fputs的返回值,结果为0)
注意: 如果使用两个 fputs(),对一个文件分别写入两个字符串,如果第一个写入的字符串末尾没有'\n',那么两个字符串将会在同一行进行写入。
4、fgets
既然能一行一行地写入,那么必然能一行一行地读取:查阅c++reference,结果如下
这里不再进行过多地截取,可以自行查阅
cplusplus网站:
cplusplus.com - The C++ Resources Networkhttps://legacy.cplusplus.com/
- 返回值为读取到地字符串的地址,类型为char*
- 读一个字符串,然后将字符串存入str所指向的数组,直到遇到换行符或者文件末尾
- 使用int num来控制读取字符串的长度。
- fgets读取遇到换行符停止读取,但是函数认为他是有效字符,并将其一起复制到str指向的字符串。
- 在读取完字符后,str后面会自动追加终止字符。
- 如果读取错误,则返回值为一个空指针,成功则返回的是str。
直接上代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
char arr[100] = { 0 };
FILE* pf = fopen("ilike.txt", "r");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
fgets(arr,23,pf);
fclose(pf);
pf = NULL;
}
printf("%s", arr);
system("pause");
return 0;
}
这里,文件里面原来的数据为a-z,26个小写字母。我们使用fgets(arr,23,pf)来读取前23位
并打印arr,结果如下:后三位如预测结果一样未打印。
注意:这里不管是每行还是单个字符读取,每次操作完,位置指示器都会指向下一个将被读取的目标。
例如读取 文本文件中存在这样一串数据 : abcdefgh
使用fgets 读取前五个字符,原本位置指示器指向a,在读取五个字符后,位置指示器指向f所在的位置
对于文件的操作如果只是单个字符或者单个字符串的进行读取和写入,那么对文件的操作的精准度就不会很高,所以定义格式化输入输出函数来对文件进行操作:
5、fprintf
将文件格式化输出,其定义如下
stream为要写入的文件流,后面紧跟要输入数据的格式和对应的数据。
直接上代码:我们格式化写入一个结构体类型的数据。
- 如果写入成功,则返回写入字符的总数
- 如果写入错误,则设置指示符FERROR并返回一个负数
#include<stdio.h>
#include<stdlib.h>
struct S
{
char name[20];
int age;
};
int main()
{
S s = { "xiaoxin",18 };
FILE* pf = fopen("ilike.txt", "w");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
fprintf(pf,"%s %d",s.name,s.age);
fclose(pf);
pf = NULL;
}
system("pause");
return 0;
}
对应目录里ilike.txt文件的内容被修改为:
我们可以将其和printf做一个对比
发现唯一不同的就是在前面的参数,fprintf多了一个FILE*类型的指针变量,这个变量就是用来定位你所要写入的文件。
6、fscanf
同样的,我们首先将其和scanf做一个对比:
同样是多了一个指向文件的FILE*类型的指针。
- 写入成功,函数返回参数列表中成功写入的项数
- 如果读取发生错误或者读到了文件末尾,则设置正确的指示符feof,
- 如果在成功读取任何数据之前,都会返回EOF
我们将之前的写入的结构体变量进行写入操作,并将读取到的数据赋值给另外一个同名结构体不同变量a,并打印。
#include<stdio.h>
#include<stdlib.h>
struct S
{
char name[20];
int age;
};
int main()
{
S a = { "xiaoxin",18 };
FILE* pf = fopen("ilike.txt", "r");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
fscanf(pf,"%s %d",&a.name,&a.age);
fclose(pf);
pf = NULL;
}
printf("%s %d", a.name, a.age);
system("pause");
return 0;
}
结果如下:
7、文件操作函数操作于屏幕的方法
细心的人应该会发现,上面所列的文件操作函数的表格中对应位所有输出输入流,那么有人应该会去思考,这些文件操作函数是否可以作用于普通的键盘输入屏幕输出操作。
继续甚于研究,其实对于任何一个c程序,只要运行起来,就会默认打开三个流:
stdin -- 标准输入流 -- 键盘
stdout -- 标准输出流 -- 屏幕
stderr -- 标准错误流 -- 屏幕
它们三个均为FIEL*类型,也就是一个文件指针,那么我们就可以利用上面的函数对齐进行操作
那么该如何操作? 看下面的代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int car = fgetc(stdin);
fputc(car, stdout);
system("pause");
return 0;
}
其结果如图。输入5,输出5
相对的,对字符串的操作:
#include<stdio.h>
#include<stdlib.h>
int main()
{
char arr[20] = { 0 };
fgets(arr,8,stdin);
fputs(arr, stdout);
system("pause");
return 0;
}
结果为:
8、二进制读写
1、fwrite
解释:
- ptr指向要写入的元素的地址,
- size_t size 为每一个要写入元素的大小(单位:字节)
- size_t count 元素数
- 如果成功写入则返回总元素数
- 如果这个返回值于元素总数的参数不同,则表示写入错误。
- 如果每个元素大小,或者总元素个数为0,则返回0.
我们写入一个上面写过的结构体到ilike.txt文件当中去
#include<stdio.h>
#include<stdlib.h>
struct S
{
char name[20];
int age;
};
int main()
{
S a = { "xiaoxin",18 };
FILE* pf = fopen("ilike.txt", "w");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
fwrite(&a, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
}
system("pause");
return 0;
}
打开文件如图:
因为是二进制的形式,所以用二进制读fread来读取。
2、fread
- 从文件流中读取数据块
- 从流中读取元素,每个元素大小为size个字节,一共读取count个元素
- 读取到的元素将会被存储到ptr指向的内存当中
- 如果读取成功,则返回读取到的元素的总数
- 如果这个返回值和要读取的元素总数count不同,说明在读取玩到count个元素之前就发生了读取错误或者提前遇到了文件末尾
- 如果size或count为0,则返回值也为0
我们继续读取上次以二进制写入的xiaoxin 18,
#include<stdio.h>
#include<stdlib.h>
struct S
{
char name[20];
int age;
};
int main()
{
//S a = { "xiaoxin",18 };
S tem = { 0 };
FILE* pf = fopen("ilike.txt", "r");
if (pf == NULL)
printf("pf fopen \"ilike\" fail");
else
{
//fwrite(&a, sizeof(a), 1, pf);
fread(&tem, sizeof(tem), 1, pf);
fclose(pf);
pf = NULL;
}
printf("%s %d", tem.name, tem.age);
system("pause");
return 0;
}
结果为:
五、随机读写
也称为选择读写。如果存在一串数据“a-z”的26个小写字母,如果我不想读取字母abc三个字母,那么就要用到位置指示器(position indicator),也就是文件指针。
1、fseek
fseek,顾名思义也就是,file seek 的意思,文件搜索:
- stream为需要操作的文件流
- offset,偏移量,从原点偏移的字节数
- origin,起始位置,也就是文件指针当前所指向的位置
- 如果读取成功,函数返回0
- 否则,返回一个非0值
这个
origin对应的有三个constant值 分别为: SEEK_SET SEEK_CUR SEEK_END
对应 文件头 文件中间 文件尾(EOF所在位置)
举例,我们在ilike.txt文件中写入abcdef,使用feek读取:
预测: fseek ( pf, 3 , SEEK_SET) 读取为 d
fseek ( pf, -4 , SEEK_END) 读取为 c
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf = fopen("ilike.txt", "r");
if (pf == NULL)
{
printf("pf fopen fail");
}
fseek(pf, 3, SEEK_SET);
int ch = fgetc(pf);
printf("%c", ch);
fseek(pf, -4, SEEK_END);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
system("pause");
return 0;
}
结果为:
2、ftell
有时候对文件多次操作后,不知道文件指针指向了什么位置,这个时候就可以用ftell函数来解决
返回类型为偏移量。我们在上面的fseek代码的情况下加上ftell:
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf = fopen("ilike.txt", "r");
if (pf == NULL)
{
printf("pf fopen fail");
}
fseek(pf, 3, SEEK_SET);
int ch = fgetc(pf);
printf("%c ", ch);
fseek(pf, -4, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
int pos = ftell(pf);
printf("%d", pos);
fclose(pf);
pf = NULL;
system("pause");
return 0;
}
最后结果为 :3
我们可以看出来c对于起始位置的偏移量确实为3,因为c在读完之后,文件指针会向后偏移一位。
3、rewind
对于不知道文件指针最后指向什么地方,也可以使用rewind将文件指针回调到最初位置。
本次内容完......