目录
一、文件的基础知识
1、什么是文件
在日常生活中我们所说的文件就是电脑C盘、D盘上的各种文件,但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。
数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件。在本节中我们学习的就是数据文件。
2、文件有什么用
文件可以保存数据,使数据能做到持久化存储。文件可以使我们的操作更为合理,比如现在写的这篇博客,本质就是一个文件,不过是存储在服务器上的文件(数据)。电子文件的最大特点就是易于检索了,这也正是电脑的优点之一。之后我要用到之前学到的动态内存管理来写一个通讯录管理系统代码,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次再次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。 这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
3、文件的格式是什么
所有文件都有唯一的标识符,标识符可以分为三部分:文件路径+文件名主干+文件后缀,比如存储在我电脑中的VS文件标识符为:
C:\Program Files\Microsoft VisualStudio\2022\Community\Common7\IDE
为了方便称呼,我们一般将其称为文件名,比如 devenv.exe 就是一个典型的程序文件 。
二、文件的打开和关闭
文件指针
每个被使用的文件都会在内存中开辟一个对应的文件信息区,用来存放文件的相关信息(如文件的名字,状态及当前的位置等);这些信息被保存在一个结构体变量中,该结构体类型被系统声明为FILE;不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心FILE中的细节。一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。在缓冲文件系统中,关键的概念是 “文件类型指针”,简称 “文件指针“。
FILE* pf; //文件指针变量
pf 是一个指向FILE类型数据的指针变量,它可以使 pf 指向某个文件的文件信息区(本质上是一个结构体变量),通过该文件信息区中的信息就能够访问该文件;也就是说,通过文件指针变量能够找到与它相关联的文件。
文件的打开与关闭的函数
文件得先打开,才能关闭,最好跟动态内存管理一样,有申请就要有释放,成对出现更为安全,ANSIC 规定使用 fopen 函数来打开文件,fclose 函数来关闭文件;在编写程序的时候,我们在打开文件的同时,fopen 函数会返回一个 指向该文件的 FILE* 的指针变量,从而建立指针和文件的关系。
1、打开文件fopen
函数声明:
FILE * fopen ( const char * filename, const char * mode );
返回类型是FILE*文件指针,也相当于建立了指针和文件的关系。打开失败会返回一个空指针-
NULL
,所以需要对fopen的返回值进行判断。1、参数
filename
为文件名2、参数
mode
为打开方式
目标文件
有两种形式,一种是绝对地址,另一种是相对地址
绝对地址
即唯一路径,使用绝对地址访问文件时,文件可以在电脑中的任意位置,前提是地址要合法。绝对位置的文件标识符必须全,即文件路径+文件名主干+文件名后缀。
比如:
//绝对,指此地址是唯一的,能通过这个地址找到唯一的文件
FILE* fp = fopen(" F:\c代码\通讯录管理系统(文件)\test.txt", "w");
相对地址
此时的路径是固定的,一般和当前源文件处于同一位置,相对嘛,就是相对于当前程序文件。相对位置只需要文件名主干+文件后缀就行了 。
比如:
//相对,指在当前工程文件内的文件
FILE* fp = fopen("test.txt", "w");
文件的打开方式: (有这么几种,需要记住)
2、关闭文件fclose
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件,c语言中关闭文件的函数为fclose
。
函数声明:
int fclose ( FILE * stream );
- 函数关闭文件成功会返回
0
,失败会返回EOF
(一般不存在失败情况)。- 函数参数
stream
为指向文件信息区的文件指针,一般为fopen的返回值。- 关闭文件后为了预防野指针问题,通常将stream指针置为空。
具体案例如下:
int main()
{
//打开文件
//相对路径
//FILE* pf = fopen("test.txt", "w");
//绝对路径
FILE* pf = fopen("F:\\c代码\\text_10.13\\test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们以只读的形式打开,由于我代码路径下没有test.txt这个文件,所以它会先创建一个test.txt文件,然后再把Hello,world 这句话写入到到test.txt中。
三、文件的顺序读写
c语言有关于文件的操作函数有如下几个:
1、输入输出流:
在介绍文件读写操作前,我们先要来介绍一下“流stream”的概念,假设将数据看作水流,那么它就有两个关键部分:从哪里流出(源头)、流入哪里(终点),其中流出可以看作输出,流入可以看作输入。C语言中有三种流:标准输入输出流、文件输入输出流、二进制输入输出流(实际使用时用前两种流,第三种的目标流一般为文件)。下面我来一一介绍:
(1)标准输入输出流:
标准输入输出流(I/O)包括标准输入流(stdin)—从键盘输入、标准输出流(stdout)—从屏幕输出、标准错误流(strerr)—从屏幕输出,任何一个C程序,只要运行起来都会默认打开以上三个流,比如我们常用的 scanf、printf就是基于标准输入输出流而运行的,这也正是二者需要键盘、屏幕的原因。
(2)文件输入输出流:
顾名思义,文件输入输出流所依赖的载体为文件,无论是输入还是输出数据,都是在文件上进行的,因此它的对象类型为 FILE* ,文件输入输出流可以使用所有输入输出流函数,比如fputc、fprintf、fscanf等,使用时只需要加上目标流类型就行了。
(3)二进制输入输出流:
二进制输入输出流主要适用于文件操作,对文件进行二进制数据的读取和写入,所以二进制输入输出一般用在文件操作中。二进制只有0、1这两个数,因此如果我们使用二进制输出流对某个文件进行写入,文件中存储的信息就变成了一串二进制数(可以使用二进制文件查看器观察),如果用普通文本的形式查看此文件,会得到一串乱码。二进制输入输出流有fwrite、fread这两个函数。
接下来就开始一一讲解这些函数了
2、fgetc 与 fputc
fgetc:从文件中读取一个字符到内存中;fputc:从内存中输出一个字符到文件中。
fgetc函数声明:
int fgetc( FILE *stream );
1、stream 对应文件指针
2、int 函数返回值,读取成功时返回对应字符,读取失败或者遇见文件末尾时返回EOF。
fputc函数声明:
int fputc( int c, FILE *stream );
1、c 要操作的字符
2、stream 对应文件指针
3、 int 函数返回值,返回输出的字符
来看看具体代码(fputc):
//写入26个英文字母
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");//只写的打开方式
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a'+i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(fgetc):
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
int i = 0;
/*for (i = 0; i < 26; i++)
{
int ch = fgetc(pf);
printf("%c ", ch);
}*/
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3、fgets和fputs :
fgets:从文件中读取一行字符到内存中;fputs:从内存中输出一行字符到文件中。
char *fgets( char *string, int n, FILE *stream );
1、string 数据的存储位置
2、n 要读取的最大字符数
3、stream 对应文件指针
4、 char* 函数返回值,读取成功返回字符串首字符的地址,失败或者遇到文件末尾返回NULL
int fputs( const char *string, FILE *stream );
1、 string 要输出的字符串的首地址
2、 stream 对应文件指针
3、 int 函数返回值,输出成功,返回一个非负值,否则返回EOF
我们来看下具体代码(fputs):
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
//写文件-一行一行写
fputs("hello\n", pf);
fputs("bitejiuyeke\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(fgets):
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
//要一行一行的读
char ch[20] ={0};
fgets(ch, 20, pf);
printf("%s", ch);
fgets(ch, 20, pf);
printf("%s", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意:
1、文件的读和写不能同时进行;
2、对于同一个文件,当我们以写的形式打开时,操作系统首先会将该文件中原有的数据全部清除,然后再执行后续操作;(所以上面test.txt中原有的ab不见了)
3、对于 fgets 函数来说,实际从文件中读取的字符的个数会比指定的字符个数少一个,因为最后一个字符会被用于字符串的结束标志’\0’;
4、fscanf 与 fprintf
fscanf:把文件中的数据格式化的读取到内存中;fprintf:把内存中的数据格式化的写入到文件中;
int fscanf( FILE *stream, const char *format [, argument ]... );
1、 对于fscanf与scanf的使用相似,区别是fscanf前面增加了指向输入流的指针stream
2、int 函数返回值,成功时,函数返回成功填充的参数列表的项数,读取失败或者遇到文件末尾返回EOF
int fprintf( FILE *stream, const char *format [, argument ]... );
1、对于fprintf与printf的使用相似,区别是fprintf前面增加了指向输出流的指针stream
2、 int 函数返回值,成功时,返回写入的字符总数,失败时返回一个负数
来看看具体代码(fprintf):
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 18, 93.8f };
//把s中的数据写到文件中
FILE*pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
(fscanf):
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = {0};
//把s中的数据写到文件中
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
5、二进制读写文件 (fwrite 与 fread)
fwrite:以二进制的形式向文件中写入数据;fread:以二进制的形式从文件中读取数据;
函数声明:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
1、ptr 指向要写入的空间的起始地址
2、size-指要写入的每个元素的字节大小。
3、count-元素的数量,每个元素的大小为字节。4、stream-指向指定输出流的FILE对象的指针。
5、写入成功返回写入的元素总数。如果返回值与count参数不同,则写入错误,在这种情况下,将为流设置错误指示器(feror)。如果size或count为零,则函数返回零,错误指示符保持不变。
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
1、参数与 fwrite 相同
2、成功读取返回的元素总数。如果返回值与count参数不同,则可能发生读取错误,也可能在读取时到达文件结尾。在这两种情况下,都设置了适当的指示器,可以分别用feror和feof进行检查。如果size或count为零,则函数返回零。
我们来看看具体代码(fwrite):
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 18, 93.8f };
//把s中的数据写到文件中
FILE*pf = fopen("test.txt", "wb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//二进制的写文件
fwrite(&s, sizeof(s), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
由此可看到二进制写入的文件是乱码,接下来看看二进制读取文件的具体代码(fread):
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = {0};
//把s中的数据写到文件中
FILE* pf = fopen("test.txt", "rb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//二进制的读文件
fread(&s, sizeof(s), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
测试可知二进制文件用fread函数可以打印在屏幕中。
四、扩展:sscanf和sprintf
我们已经学了scanf与printf函数,fscanf与fprintf函数,再来看一组函数:sscanf与sprintf
函数声明:
int sprintf ( char * str, const char * format, ... );
作用:把格式化的数据按照一定的格式转换成字符串,存储在str
指向的缓冲区中。
函数声明:
int sscanf ( const char * s, const char * format, ...);
作用:从字符串中按照一定的格式独处取出格式化的数据,并根据参数格式将其存储到附加参数指定的位置。
#include <stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
char buf[100] = {0};
struct S tmp = { 0 };
struct S s = { "张三", 20, 95.5f };
//能够把这个结构体的数据,转换成字符串
//"张三 20 95.5"
sprintf(buf, "%s %d %f", s.name, s.age, s.score);//以字符串的形式打印
printf("%s\n", buf);
//将buf中的字符串,还原成一个结构体数据
sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);//以结构的形式打印
return 0;
}
从printf打印的格式不同就可以看出,sprintf与sscanf作用不一样,但是打印的结果是一样的。
scanf / fscanf / sscanf 三者的区别与联系 :
scanf:scanf 函数是格式化输入函数,只适用于标准输入流(键盘、屏幕);
fscanf:scanf 函数也是格式化输入函数,不过它适用于所有输入流;
sscanf:sscanf 是专门针对字符串操作函数,用于将字符串数据转换为格式化的数据;
prinft / fprintf / sprintf 三者的区别与联系:
prinft :prinft 函数是格式化输出函数,只适用于标准输入流(键盘、屏幕);
fprintf: fprintf 函数也是格式化输出函数,不过它适用于所有输入流;
sprintf :sprintf 是专门针对字符串操作函数,用于格式化的数据转化为字符串;
五、文件的随机读写
在上面介绍了文件顺序读写的相关函数及操作,但是要高效率的使用文件中的数据,只会顺序读写显然是不够的,我们还得学会文件的随机读写。
1、fseek函数
函数声明:
int fseek ( FILE * stream, long int offset, int origin );
1、stream 对应文件指针
2、offset 相对于origin参数的偏移量
3、 origin 偏移量的参考位置
4、int 函数返回值,设置成功返回0,否则返回非0
origin有三种选择:
(1)SEEK_SET 文件的开始
(2)SEEK_CUR 当前指向的位置
(3)SEEK_END 文件的末尾
来看看具体的使用代码:
int main()
{
//以写的形式打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//将字符 'a' - 'z' 写到文件中去
int i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
//以读的形式打开文件
FILE* pf1 = fopen("test.txt", "r");
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
//默认文件指指示器指向第开始的位置
char a = fgetc(pf1);
printf("%c\n", a);//读出的结果就是a
fseek(pf1, 1, SEEK_SET);
a = fgetc(pf1);
printf("%c\n", a); //b
//读取之后文件指示器指向c
fseek(pf1, 5, SEEK_CUR);
a = fgetc(pf1);
printf("%c\n", a); //h
//设置文件指针指向最后一个字符
fseek(pf1, -1, SEEK_END);
//读出该字符
char ch = fgetc(pf1);
printf("%c\n", ch);//z
//关闭文件
fclose(pf1);
pf1 = NULL;
return 0;
}
2、ftell 函数
函数功能:
返回文件指针相对于起始位置的偏移量。
函数声明:
long int ftell ( FILE * stream );
long int 函数返回值,正常时返回函数指针相对于起始位置的偏移量,出错时返回-1L
int main()
{
//以读的形式打开文件
FILE* pf1 = fopen("test.txt", "r");
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
//设置文件指针指向最后一个字符
fseek(pf1, -1, SEEK_END);
//求出文件指针相对于起始位置的偏移量
int ret = ftell(pf1);
printf("%d\n", ret);
//关闭文件
fclose(pf1);
pf1 = NULL;
return 0;
}
可以看出z对于起始位置的偏移量为25
3、rewind 函数
函数功能:
让文件指针的位置回到文件的起始位置。
void rewind ( FILE * stream );
stream对应文件指针