绪论
书接上回,上章的动态内存管理,通过过几个函数来对内存的堆栈开辟一定的空间进行使用,但始终还是借操作系统的内存最终还是要还回去的(当程序结束后),而本章将讲到C语言如何操作文件就是来进行持久化的保存所创建的数据,并且我还会通过学生信息管理系统来进行实操。
话不多说安全带系好,发车啦(建议电脑观看)。
附:红色,部分为重点部分;蓝颜色为需要记忆的部分(不是死记硬背哈,多敲);黑色加粗或者其余颜色为次重点;黑色为描述需要
思维导图:
要XMind思维导图的话可以私信哈
目录
1.为什么要使用文件
使用文件时可以持久化的将数据存储起来,也就可以在程序结束后的下一次运行时调用他(其存放在硬盘上(文件、数据库),不像内存中的数据会自动销毁)
2.什么文件
在磁盘中的文件其实都是文件只是类型不相同
而在我们程序设计中一般关注两种文件:
2.1程序文件:
一般来说是源文件(后缀为.c文件)、目标文件(后缀为.obj)、可执行程序(后缀为.exe)但在程序文件内的所有文件也都可称为程序文件
2.2.数据文件:
在一个程序中当我们读取某些文件的数据或者对某些文件进行修改,这些输入(读取)/ 输出(修改)的文件就被称为数据文件
2.3文件名
知识点:
文件名(文件标识)是由:文件路径 + 文件主干 + 文件后缀 组成
如上这个文件其名是:C:\code\代码仓库\双向循环列表\双向循环列表\test.c
直接写出文件名的路径叫绝对路径 : "C:\\code\\代码仓库\\文件操作章\\文件操作章\\test.txt"
注意双杠
不过一般只用test.c(直接写 主干+后缀 的数据文件这种路径叫做相对路径 : "test.txt")
假如文件在程序文件上一级则可以在相对路径前+" ../ " : "../test.txt" 以此类推,几个就向上找几级
本章节主要讨论的就主要是数据文件,我们应该如何在程序中进行文件的读取和改写。
对于向scanf、printf这类都属于终端型(标准输入输出流)的,而本章是将数据写到文件中去的。
3.文件的打开和关闭
3.1文件指针
知识点:
对于每个文件来说每当我们在程序中打开一个文件时都会开辟一个文件信息区(存放文件的信息),而在C语言的内存中每当我们用函数打开一个文件时就会创建一个结构体来存储这个文件而这个文件的信息就存在文件信息区,并且该结构体的类型的系统声明成为FILE,所以一般都是直接通过FILE * 指针来控制(维护)
对于文件信息区可以大概的看成上图,并且结构体中的内容我们可以暂不关心(因为在不同的编译器下FILE结构体中的成员是不一样的,但大同小异)。
3.2打开文件、关闭文件的函数
知识点:
3.2.1fopen(打开文件函数):
FILE * fopen ( const char * filename, const char * mode );
filename:文件名
mode:打开方式
"r":读
"w":写
"a":追加
"r+":读/刷新
.....
返回类型是一个FILE * 的指针,因当打开一个文件时他就会在文件中创建一个文件信息区(打开文件名的信息),所以要把文件信息区的地址返回来(为了方便我们使用信息),所以同时需要一个相同类型的指针来接收如其地址 FLIE* pf = fopen(test.txt,"r");
并且当打开失败时,会返回一个NULL,所以像动态内存申请空间一样要加上一个判断来判断是否打开成功
3.2.2fclose(关闭打开的文件):
int fclose ( FILE * stream );
stream:是已经打开了的信息区,我们将其地址传过去即可关闭:(fclose(pf));
若关闭成功会返回0,关闭失败则会返回EOF
fopen 和 fclose 的头文件都是#include<stdio.h>
用代码来大概展示一下如何打开和关闭文件:
int main()
{
//FILE * pf = fopen("C:\\code\\代码仓库\\文件操作章\\文件操作章\\test.txt","r");//绝对路径
FILE * pf = fopen( "test.txt","r");//相对路径
if(pf == NULL)//防止开辟失败,访问NULL
{
perror("fopen");
return 1;
}
//使用文件 此处省略
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
细节:
文件的打开方式:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w" (只写) | 为了输出数据,打开一个文本文件 | 创建一个新的文件 |
"a"(追加) | 向文本文尾添加数据 | 创建一个新的文件 |
"rb"(只读) | 为输入数据,打开一个二进制文件 | 出错 |
"wb" (只写) | 为输出数据,打开一个二进制文件 | 创建一个新的文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
"r+"(读写) | 为了读和写,打开一个文件文本 | 出错 |
"w+" (读写) | 为了读和写,打开一个新的文件 | 创建一个新的文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 创建一个新的文件 |
"rb+"(读写) | 为了读和写打开一个二进制文件 | 出错 |
"wb+" (读写) | 为了读和写,新 建一个二进制文件 | 创建一个新的文件 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读和写 | 创建一个新的文件 |
4.文件的顺序读取
4.1顺序读取函数
知识点:
输出:内存中输出到文件中/屏幕中(put,write、prinf)
fprintf从指定格式的数据内存得到数据输出到文件里、printf从指定格式的数据内存得到数据输出在屏幕上,
输入:从键盘/文件中读取到输入到内存中(read、get、scanf)
fscanf从文件得到输入到指定内存再进行printf,scanf从键盘上得到输入在指定内存
头文件都一样是#include<stdio.h>
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | int fgetc ( FILE * stream ); | 所有输入流 |
字符输出函数 | int fputc(int character,FILE * stream) | 所有输出流 |
文本行的输入函数 | char * fgets ( char * str, int num, FILE * stream ); | 所以输入流 |
文本行的输出函数 | int fputs ( const char * str, FILE * stream ); | 所有输出流 |
格式化输入函数 | int fscanf ( FILE * stream, const char * format, ... ); | 所有输入流 |
格式化输出函数 | int fprintf ( FILE * stream, const char * format, ... ); | 所有输出流 |
二进制输入函数 | size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); | 文件 |
二进制输出函数 | size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ); | 文件 |
附:
流(stream):
当我们在写代码时,我们只需要把代码通过函数写好后在流中就可以实现我们需要的结果,这是因为C语言已经封装好的程序(流),一般来说当运行一个C语言程序时会自动打开
3个流(这也对应这scanf、printf、perror):
stdin - 标准输入(键盘) 类型 FILE *
stdout - 标准输出(屏幕) FLIE *
stderr - 标准错误(屏幕) FLIE *
所以我们在每次写程序控制文件的时候就必须先打开文件也就拥有指向这个文件的流
才能进行文件的输入(文件的打开方式"r",就对应像stdin的这样的输入流)、输出(打开文件的方式"w",对应输出流);所以上面的前6个里他们适合所有输入/输出流也就表明了,他们既可以对文件流中使用,也可以在标准流中使用
具体如下:
标准流时:
#include<stdio.h>
int main()
{
int ch = fgetc(stdin);
fputc(ch,stdout);
return 0;
}
细节:
文件流时:
4.1.1.fgetc、fputc
他们返回的都是整形也就是字符所对应的ASCII码值
int fgetc ( FILE * stream ) ; int fputc(int character,FILE * stream);
而他们的参数 FILE * stream 是指对应文件信息区的地址、int character 是所要写的字符的ASCII码值(直接传字符进去进去后都会转成ASCII码值)
练习:
将a~z的英文字母存放在文件中,并读取后打印
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");//同过写的方式来打开文件,在每次写的时候都相当于重新创建一个文件,将原文件数据覆盖
if (pf == NULL)//防止开辟失败,访问NULL
{
perror("fopen");
return 1;
}
//使用文件 此处省略
for (int i = 'a'; i <= 'z';i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
FILE* pf1 = fopen("test.txt", "r");//通过读的方式来读取数据
if (pf1== NULL)//防止开辟失败,访问NULL
{
perror("fopen");
return 1;
}
//使用文件 此处省略
for (int i = 'a'; i <= 'z'; i++)
{
int ch = fgetc(pf1);//返回ASCII码值并打印
printf("%c ", ch);
}
//关闭文件
fclose(pf1);
pf1 = NULL;
return 0;
}
对于fgetc来说他会按顺序一个个读取,先读最前面的读完后会自动指向下一个,当下一次再次读取时就会接着读下一个的了;对于fputc来说同样的他会一个个写,写完后自动往下指继续写,但假如重新打开文件(以写的方式)就会被覆盖。
如果读取失败/或写入失败的话将会返回EOF
4.1.2.fgets、fputs
char * fgets ( char * str, int num, FILE * stream );
他读取在stream中指定num个字符放在str中,若成功返回时返回:str、若失败了则返回EOF
参数 str 表示的是所要存/所要取字符串的位置的地址
参数 num 表示的是所要得到的字符的(num-1)个,为什么是num-1个是因为在读时字符串的最后要放上一个\0,所以会占掉一个位置;并且当遇到\n时会自动停止并且把\n换成\0,并且假如num的大小大于所要读大小会将多余的位置置为\0
参数 stream 表示的是文件信息区的地址
int fputs ( const char * str, FILE * stream );
他把str指向的字符串放到stream文件信息区上成功返回时返回:非负数;若失败了则返回EOF
这个就相对简单了就是将字符串的首地址传进去,以及文件信息区的位置
一行行的写,每一次写一行
int main()
{
FILE* pf = fopen("test.txt","w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("hello world\n", pf);//文件输出函数
fputs("hello bit\n", pf);
fclose(pf);
pf = NULL;
FILE* pf1 = fopen("test.txt", "r");
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
char ch[20] = { 0 };//
fgets(ch,12,pf1);//文件输入读取,并且此时字符串时12个字符那么就需要13个空间,因为fgets会在最后加上一个\0(但是此处只会那取 12 - 1 个字符 在最后12的位置要放上\0 )
printf("%s", ch);//hello world
fgets(ch, 10, pf1);
//此处因为一开始将前面的11(num - 1)个字符读去了在第一行还剩下一个\n所以就会把\n读取到,并且fgets遇到\n会自动停止并且将\n换成\0,并且因为只读一行所以会直接返回
printf("%s", ch);//打印\n
fgets(ch, 11, pf1);//来到下一行,此时字符串有10个空间,第11的位置放\0,所要刚好
printf("%s", ch);//打印hello bit\n
fclose(pf1);
pf1 = NULL;
return 0;
}
4.1.3.fprintf、fscanf
int fprintf ( FILE * stream, const char * format, ... );
fprintf和printf用法几乎一样只需在最前面加上所要输出的文件信息区的位置即可
返回值:成功的话是返回总字符个数、失败则返回一个负数并且报错
int fscanf ( FILE * stream, const char * format, ... );
同样用法和scanf类似只是最前面加一个文件信息区的指针位置,
对于最后的.....是可变参数列表如:printf("%d %d %d",12,13,15); 此时参数可以同时多个输出
具体用法由下面代码展示:
struct S
{
int a;
char arr[20];
float c;
};
int main()
{
struct S s = { 100,"李四",120.00 };
FILE* pf = fopen("test.txt","w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fprintf(pf,"%d %s %f",s.a,s.arr,s.c);//先输出到pf文件上
fclose(pf);
pf = NULL;
FILE* pf1 = fopen("test.txt","r");
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
fscanf(pf1,"%d %s %f",&(s.a),s.arr,&(s.c));//从文件中输入到内存中
printf("%d %s %f\n", s.a, s.arr, s.c);//从内存中打印
fclose(pf1);
pf1 = NULL;
return 0;
}
4.1.4.fread、fwrite
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
此时fread、fwrite是二进制的输入输出函数,所以输入和输出都是以二进制形式的
同样ptr表示写入 / 读取 的位置,
size 表示的是该类型的大小,
count该类型的个数,
stream文件信息区的地址。
具体使用:
//学生信息管理系统铺垫
struct S
{
int a;
char arr[20];
float c;
};
int main()
{
struct S s = { 123,"李四",321 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&s, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
struct S s1 = {0};
FILE* pf1 = fopen("test.txt", "rb");
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
fread(&s1, sizeof(struct S), 1, pf1);
printf("%d %s %f", s1.a, s1.arr, s1.c);
fclose(pf1);
pf1 = NULL;
return 0;
}
4.2sscanf、sprintf
知识点:
int sscanf ( const char * s, const char * format, ...);
sscanf从s处数据得到输入到指定内存,把字符串转化成一个格式化的数据
int sprintf ( char * str, const char * format, ... );
sprintf从指定内存中获得输出在str处,打一个格式化的数据转化成字符串
由此可以总结出:
scanf、printf,fscanf,fprintf,sscanf,sprintf
他们的不同类型的区别:
scanf类型:在最前面的参数是:所要获取数据的地方,如scanf的stdin(在此函数因为每次程序运行都会自动打开所以省略了),同样的fscanf的文件、stdin所有流、sscanf 的指定获得处 ;中间参数(const char * s)是指定的格式,最后是存放这些格式(...)的数据的地址,输入函数printf类型:在最前面的参数(...)是:输出指定格式数据的位置,如prinf中的stdout;fprintf的所有流,sprintf的指定输出处; sprint中间参数(fchar * str)是指定的格式,最后的参数是从哪里获取的数据(...),输出函数
细节:
深入了解scanf、prinf后就能写出
int main()
{
struct S s = { 123,"李四",321.0 };
char tmp[100] = { 0 };
sprintf(tmp , "%d %s %f",s.a,s.arr,s.c);//将结构体s的数据输出tmp内
printf("%s\n", tmp);
struct S s1 = { 0 };
sscanf(tmp, "%d %s %f", &(s1.a), s1.arr, &(s1.c));//将tmp得到的数据输入到结构体s1内
printf("%d %s %f", s1.a, s1.arr, s1.c);
return 0;
}
5.文件的随机读写
5.1.fseek
知识点:
int fseek ( FILE * stream, long int offset, int origin );
改变文件指针的指向
stream是文件信息区
offset是偏移量
origin是起始位置
起始位置可以是一下三种,SEEK_SET文件的开始、SEEK_CUR文件当前的位置、SEEK_END文件的末尾
细节:
通过实例来具体的解释
int main()
{
//省略写的步骤直接创建一个文件写好后直接使用
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 0;
}
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
//此时因为fgetc返回后会自动往后走一步
//所以若想重新打印a
//则可以通过fseek函数来实现,此时指向d应该往后偏移3步
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//a
return 0;
}
5.2ftell
知识点:
long int ftell ( FILE * stream );
返回此时的偏移量
细节:
很简单直接通过代码解释
5.3.rewind
知识点:
void rewind ( FILE * stream );
回到文件起始的位置
细节:
当fseek向左偏移-2打印了b后再用rewind直接回到起始位置再进行打印出a
6.文本文件和二进制文件
知识点:
二进制文件:内存中的数据是以二进制的形式存储的,如果不加以如何转化直接存到外存的文件就叫做二进制文件
文本文件:内存中的数据存储在外存前,将二进制转化成ASCII码对应的字符后再存储的文件就叫做文本文件
细节:
具体来讲就是:当我们有数值10000此时他的二进制可表示成:
00000000 00000000 00100111 00010000
若直接吧上述的二进制序列直接存储在文件中的则是二进制文件
若是把10000 分解成 5个字符 : '1' 、 '0' 、'0' 、... 后再分别把这5个字符ASCII码值进行存储的话就被称为文本文件
1ASCII码值: 49的二进制:00110001
0ASCII码值:48的二进制: 00110000 的二进制
7.文件读取结束的判定
7.1.feof函数
知识点:
当文件读取结束后可以用feof函数是用来判断读取失败的原因
细节:
fgetc读取失败返回EOF,成功返回该字符
fgets读取失败时返回NULL,把str返回(str是所要放字符串的起始位置)
fread返回实际读到的个数,所以可以通过判断返回值是否小于实际要读的个数来判断是否正确
在文件读取结束后,为了知道原因我们可以
ferror(pf)
判断是不是读取时遇到(I / O输入输出型) 错误而结束的,如果是则返回真
puts("I/O error when reading");
feof(fp))判断是不是遇到结束标志而结束的,如果是则返回真
puts("End of file reached successfully");
具体练习:
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组 , sizeof *a 表示的是一个元素的大小
fclose(fp);
double b[SIZE];
fp = fopen("test.bin","rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
if(ret_code == SIZE) {
puts("Array read successfully, contents: ");
for(int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\n');
}
else { // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
}
8.文件缓冲区
知识点:
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
只有当缓冲区满了或者自动刷新缓冲区(fflush(pf);函数刷新缓冲区、当fclose关闭文件时才也会刷新缓冲区)才能让数据从缓冲区去到数据区或文件 。
所以我们一定要关闭文件,否则有可能没有把数据存进文件中
本章完。预知后事如何,暂听下回分解。
持续更新大量C语言细致内容,三连关注哈