目录
3.3 格式化的输入输出 fscanf()和fprintf()
前言
当我们写了一个通讯录的程序时,录入的信息没办法永久保存,退出程序后信息就消失了,这是因为此刻的信息没有放入硬盘,如果想永久保存这些数据,我们就需要用到文件的知识。
一、文件初认识
1.为什么使用文件?
为了把数据信息记录下来,在不需要的时候可以删除,我们可以选择数据持久化,一般的方法有
把数据放在数据库、存放数据到数据库等方式。
使用文件我们可以把数据放在电脑硬盘上,做到了数据持久化。
2.什么是文件?
磁盘上的文件是文件磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境
后缀为.exe)。
2.2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,
或者输出内容的文件。
本文讨论的是数据文件。
其实有时候我们会把信息输出到硬盘上,当需要的时候再把硬盘上的数据读到内存中使用,这里处理的就是硬盘上的文件。
3.文件名
一个文件要有一个唯一的标识符,也就是文件名,以便用户引用和使用。
文件名包括文件路径、文件名主干、文件后缀,比如c:\game\honkai.jpg。
二、文件操作
1.文件的打开和关闭
1.1文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.
下面我们看看vs.2013里面stdio.h头文件的FILE是怎么定义的
struct _iobuf{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsize;
char* tmpfname;
};
typedef struct _iobuf FILE;
当然不同的编译器实现方法可能不一样,不过不影响我们使用,每当打开一个文件,系统会自动创建一个FILE类型的结构体,我们使用的时候,可以创建一个FILE的变量,通过文件指针变量可以找到与它相关的文件。
2.文件的打开方式
ANSIC规定使用fopen打开文件,fclose关闭文件。
FILE * fopen ( const char * filename, const char * mode );
//第一个参数是文件名,第二个是打开方式
//如果打开成功,返回文件地址,失败会返回一个空指针
int fclose ( FILE * stream );
上面是一些打开方式,使用时记得带" ",不然会寄。
下面试着打开一个文件
int main()
{
FILE* pf=fopen("game.txt","w"); //打开
if(pf==NULL)
{
perror("fopen");
return ;
}
fclose(pf); //关闭
pf==NULL; //置空
return 0;
}
这一段代码传入的文件是相对路径,即没有写文件路径,文件就会在该工程下面打开或者创建,
如果加上路径,就是绝对路径,打开会跟着路径走。
FILE* pf=fopen("c:\\game\\test.txt","w"); //记得用两个\\
3.文件的顺序读写
3.1fgets()和fputs()函数
int fgetc ( FILE * stream ); //读文件
int fputc ( int character, FILE * stream ); //写文件,返回值是int
int main()
{
FILE* pf=fopen("game.txt","w"); //按照读的方式打开
//FILE* pf=fopen("game.txt","r"); //按照写的方式打开
if(pf==NULL)
{
perror("fopen");
return ;
}
fputc('a',pf); //将a写入文件
//int m=fgetc(pf); //将pf里的字符读到m
fclose(pf); //关闭
pf==NULL; //置空
return 0;
}
fgetc()每读一次指针会向后移动一位,以便下一次读到后面的字符,当读到文件末尾时,会返回EOF。
3.2fgets()和fputs()
char * fgets ( char * str, int num, FILE * stream );
int fputs ( const char * str, FILE * stream );
代码操作一下
int main()
{
FILE* pf=fopen("game.txt","w"); //按照读的方式打开
//FILE* pf=fopen("game.txt","r"); //按照写的方式打开
if(pf==NULL)
{
perror("fopen");
return ;
}
fputs('Kiana\n',pf); //将字符串Kiana写入文件,并且换行
//char arr[100]={0};
//fgets(arr,10,pf); //将pf里的字符串读到数组arr
fclose(pf); //关闭
pf==NULL; //置空
return 0;
}
注意,"w"写的方式,在第二次打开文件进行写操作的时候,会清空之前的数据,如果想保留,可以用"a"即追加的方式打开。在用fgets()读字符时,实际读到的字符会少一个,最后一个位置会放"\0",且fgets()一次只能读文件中的一行,最后的"\n"也会读到。
3.3 格式化的输入输出 fscanf()和fprintf()
int fscanf ( FILE * stream, const char * format, ... ); //读文件
int fprintf ( FILE * stream, const char * format, ... ); //写文件
现在来使用一下
struct S
{
char name[10];
int age;
};
int main()
{
struct S s; //创建结构体变量
FILE* pf=fopen("game.txt","w"); //按照读的方式打开
//FILE* pf=fopen("game.txt","r"); //按照写的方式打开
if(pf==NULL)
{
perror("fopen");
return ;
}
fprintf(pf,"%s %d",s.name,s.age); //将结构体写入文件
//fscanf(pf,"%s %d",s.name,&(s.gae)); 读
//将pf里的字符串读到数组arr
fclose(pf); //关闭
pf==NULL; //置空
return 0;
}
其实我们可发现,这两个函数和scanf(),printf()只有第一个参数不一样,也就是文件流,那么问题来了,为什么scanf()和printf()不需要流呢?其实不是不需要,而是输入输出里以及默认打开了。
scanf和printf是标准输入输出流,比如键盘和屏幕,而在任何一个c程序里,主要运行起来,就会默认打开三个流,stdout-标准输出流,stdin-标准输入流,stderr-标准错误流,这三个流的类型是FILE*类型的。
所以适用于所有流的函数,也可以用标准输入输出流,把文件流换成标准流就可以了。
3.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 );
我们从参数分析一下,fwrite是把count个大小为size的数据从ptr里面写到文件流stream里面
fread是把count个大小为size的数据从stream里读到普通人里。
下面我们直接使用来看看使用效果
struct S
{
char name[20];
int age;
char tele[13];
};
int main()
{
struct S s = { "kiana",18,"112233" };
FILE* pf = fopen("game.txt", "wb"); //二进制打开文件
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&s, sizeof(S), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
运行后我们可以在文件中找到相应的文本,如下
因为是二进制,所以有一些数据看不到很正常,不过用fread读取,然后打印在屏幕上就看得懂。
3.5 sscanf()和sprintf()函数
int sprintf ( char * str, const char * format, ... );
int sscanf ( const char * s, const char * format, ...);
sprintf()函数是把格式化的数据写入某个字符缓冲区,sscanf()函数是从一个字符串之中读取指定格式的数据,废话不多说,直接开干
struct S
{
char name[20];
int age;
char tele[13];
};
int main()
{
struct S s = { "kiana",18,"112233" };
struct S b = { 0 };
FILE* pf = fopen("game.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char buf[100] = { 0 };
sprintf(buf, "%s %d %s", s.name, s.age, s.tele); //把格式化的数据作为字符串写入buf
printf("%s\n", buf);
sscanf(buf,"%s %d %s", b.name, &(b.age), b.tele); //把buf里面的字符串按照某种格式读出
printf("%s %d %s", b.name, (b.age), b.tele);
fclose(pf);
pf = NULL;
return 0;
}
这两个函数和scanf()sprintf()其实是很相似的,后面两个参数都是一样的。
4.文件随机读写操作
4.1.fseek()函数
int fseek ( FILE * stream, long int offset, int origin );
SEEK_SET Beginning of file 文件起始位置
SEEK_CUR Current position of the file pointer 当前文件指针位置
SEEK_END End of file * 文件的末尾位置
这个函数的功能就是重定位流上的文件指针,什么意思呢?本来文件指针默认在重文件起始位置,
但是这个函数可以改变指针起始位置。
int main()
{
FILE* pf = fopen("game.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf,3,SEEK_SET); 指针指向文件起始位置起,偏移量加3的地方
int ch = fgetc(pf);
printf("%c",ch);
fclose(pf);
pf = NULL;
return 0;
}
可以根据需要,设置不同的origin参数。
4.1 ftell()函数和rewind()函数
long int ftell ( FILE * stream );
void rewind ( FILE * stream );
ftell函数可以计算当前文件指针偏移量,rewind可以让文件指针回到文件起始位置。
三.文件的那些小知识
1.文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文
本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
2.文件读取结束的判定
2.1 被错误使用的feof
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1.文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL
例如:
fgetc判断是否为EOF
fgets 判断返回值是否为 NULL.
2.二进制文件的读取结束判断
例如:
fread 判断返回值是否小于实际要读的个数。
(gets 判断返回值是否小于实际要读的个数。
当然,不同的文件函数返回类型不同,要根据返回值去判断文件是否读取失败。
3.文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决走的。
用通俗的语言来说,缓冲区的存在是为了提高效率,因为数据每写入一次硬盘,操作系统都会调用相关功能,缓冲区把数据暂时存起来,方便后面操作系统处理。
总结
文件的相关知识就结束了,当然还有很多知识需要以后慢慢积累。