文件操作
1. 为什么使用文件
在我们平时在vs上写的代码,程序运行起来之后的所有数据都是放在内存中的,当程序退出了之后,我们通过键盘输入的所有的数据就消失了,等我们再次执行程序后,又得要重新从键盘上敲,这样当程序的数据比较多的时候是非常麻烦的。
所以我们如果想要保留我们输入的数据的时候,只有我们自己选择删除数据的时候,数据才才会被删除。
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有:把数据存放在磁盘文件、存放到数据库等方式。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2. 什么是文件
磁盘上的文件是文件。
就比方说我们C盘和D盘中的文件,后缀为.png的图片文件,后缀为.exe的等等。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
就比如说我现在写了一个冒泡排序,我创建了一个test.c,还创建了一个bubbleSort.c,又创建了一个bubbleSort.h,这三个文件就都是程序文件,执行之后又在对应目录下自动创建了一个test.exe的可执行程序,这些都是程序文件。
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
接着上面的,我在对应的目录下创建了一个data.txt的文件,然后可以通过test.exe来对data.txt进行读(输入)数据和写(输出)数据。
而在以前我们没有创建专门的读写文件的时候,所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\mycode\test.txt
这里的c:\code\mycode\就是文件路径,test就是文件名主干,.txt就是文件后缀。
文件中不能包含这些字符:/ \ * ? " < > | :
文件的后缀名决定了一个文件的默认打开方式
文件路径指的是从盘符到该文件所经历的路径中各符号名的集合
为了方便起见,文件标识常被称为文件名。
3. 文件的打开和关闭
3.1 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
什么是文件信息区?
看图解:
pf这个指针,指向文件信息区,然后我们就可以通过文件信息去来对data.txt这个文件来进行读写操作。
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
比如下面的这个是vs2013提供的文件类型声明。
struct _iobuf
{
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
其实这就是一个结构体,只不过用处比较独特。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
3.2 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
那么文件的一般操作是:
- 打开文件
- 文件操作(读/写)
- 关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件
fopen
FILE *fopen( const char *filename, const char *mode );
fopen函数的第一个参数是文件名,第二个参数是打开方式。
fclose
int fclose ( FILE * stream )
fclose函数的参数里放的是文件名。
文件的打开方式:
“r”(只读) 为了输入数据,打开一个已经存在的文本文件
如果指定文件不存在则出错
“w”(只写) 为了输出数据,打开一个文本文件
如果指定文件不存在则建立一个新的文件
“a”(追加) 向文本文件尾添加数据
如果指定文件不存在则建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件
如果指定文件不存在则出错
“wb”(只写) 为了输出数据,打开一个二进制文件
如果指定文件不存在则建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据
如果指定文件不存在则出错
“r+”(读写) 为了读和写,打开一个文本文件
如果指定文件不存在则出错
“w+”(读写) 为了读和写,建议一个新的文件
如果指定文件不存在则建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写
如果指定文件不存在则建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件
如果指定文件不存在则出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件
如果指定文件不存在则建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写
如果指定文件不存在则建立一个新的文件
其实上面的只要记住
以"r"的方式打开,如果没有文件,就出错。如果有文件就打开
以"w"和"a"的方式打开,如果没有文件就自动新建一个文件,如果有文件就打开。
来个例子:
4. 文件的顺序读写
顺序读写意思就是,你不论输入还是输出,都是按着文件内容挨着顺序读写的。
顺序读写有这些函数:
字符输入函数
fgetc 适用于所有输入流
字符输出函数
fputc 适用于所有输出流
文本行输入函数
fgets 适用于所有输入流
文本行输出函数
fputs 适用于所有输出流
格式化输入函数
fscanf 适用于所有输入流
格式化输出函数
fprintf 适用于所有输出流
二进制输入
fread 适用于文件
二进制输出
fwrite 适用于文件
上面提到了输入输出流这个东西,我们这里简单说一下。
首先,流是一个抽象的概念
“流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据的抽象描述。
在计算机系统中是指信息从外部输入设备向计算机内部输入,或者从内存向外部输出设备输出的过程。这种输入输出的过程被形象的比喻为“流”。
我们读写数据都是靠流这个东西来实现的,比如说我们可以从文件/屏幕/网络/打印机或者其他外部设备来读写。
我们读写文件的流就是文件流,我们可以向文件内部写数据,也可以从文件内部读数据,而这些读写都是以内存的角度来说的。
还有一个流叫做标准输入/出/错误流。
标准输入流(stdin)就是我们的键盘。
标准输出流(stdout)就是我们的屏幕。
标准错误流(stderr)也是屏幕。
而C程序执行起来之后会默认打开这三个流,这一就是为什么我们执行了程序之后,不需要再进行什么打开键盘,打开屏幕这种操作。
上面的函数怎么用呢?
我们挨个来看:
fputc
这个函数的功能是将字符写入流,并前进位置指示器。
位指示器是指我们在流中的指针的当前位置。
上面就是文件流,位指示器是a的位置处。
fgetc
这上面的大概意思就是:
从流中获取字符
返回指定流的内部文件位置指示符当前指向的字符。然后,内部文件位置指示器将前进到下一个字符。
如果调用时流位于文件末尾,则该函数返回 EOF ,并为流设置 (feof) 的文件结束指示器。
如果发生读取错误,该函数将返回 EOF ,并为流设置错误指示器 (ferror)。
EOF实际的数值是-1。
我们来看一下怎么使用。
fputs
这个函数的功能是将字符串写到流中。
该函数从指定的地址 (str) 开始复制,直到到达终止空字符 (‘\0’)。此终止空字符不会复制到流中。
fgets
这个函数功能是从流中读取字符并将其作为 C 字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件末尾,以先发生者为准。
换行符使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
空字符(‘\0’)会自动附加到复制到 str 的字符之后。
fscanf
这个函数的功能是从流中读取数据,并根据参数格式将其存储到附加参数指向的位置。
fprintf
这个函数就是往流里面写数据。
上面的这些函数都是以文本形式写入文件的。
而下面的这两个函数是以二进制的形式写入文件的。
fread
这个函数将流中的数据以格式化的形式存放到ptr这个指针中。
四个参数的介绍:
ptr指向大小至少为 (sizecount) 字节的内存块的指针,转换为 void。
size要读取的每个元素的大小(以字节为单位)。
count元素数,每个元素的大小为字节大小。
stream指向指定输入流的 FILE 对象的指针。
改函数的返回值:
返回成功读取的元素总数。
如果返回的数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都会设置指标,可以分别用 ferror 和 feof 进行检查。
如果大小或计数为零,则该函数返回零,并且流状态和 ptr 指向的内容保持不变。
fwrite
这里的参数和fscanf基本一样,只不过ptr没有了大小限制。
上面函数的例子都是以文件流来演示的,我们下面就用一下标准流来看看。
fputc
fprintf
fscanf
我们来看下这些函数:
scanf / fscanf / sscanf (输入)
printf / fprintf / sprintf (输出)
这里先讲一下sscanf和sprintf。
这个函数可以将字符串转换成格式化的数据。
这个函数将格式化的数据转化成字符串。
看例子:
它们的区别是什么呢?
scanf是针对标准输入流(stdin)的格式化的输入函数。
printf是针对标准输出流(stdout)的格式化的输出函数。
fscanf是针对所有输入流(文件流/stdin)的格式化输入函数。
fprintf是针对所有输出流(文件流/stdout)的格式化输出函数。
sscanf可以将字符串转换为格式化的数据。
sprintf可以将格式化数据转换成字符串。
5. 文件的随机读写
上面的文件操作函数都是顺序读写的,我们也可以用下面这些函数来进行随机读写。
fseek
这个函数可以重新定位流位置指示器。简单理解就是重新设置文件里面的指针指向的位置。
三个参数,第一个是流,第二个是偏移量,第三个是开始重新设置的位置。
第三个参数有三种选择:
SEEK_SET 文件开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件结尾
我们通过第三个参数,假如说传的是SEEK_CUR,那么我们第二个参数传正数,就是往后偏移,如果传的是负数,就是往前偏移。
根据文件指针的位置和偏移量来定位文件指针的位置。
再来个例子:
ftell
返回文件指针相对于起始位置的偏移量。
rewind
让文件指针的位置回到文件的起始位置。
6. 文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
例子:
上面这是二进制存储,如果用ASCII表示的话就是
上面的每一格代表一个数字的ASCII,按顺序就是1 0 0 0 0。
7. 文件读取结束的判定
feof
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
- 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL . - 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
正确的使用:
以文本文件为例:
int main()
{
//打开文件
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
exit(-1);
}
//文件操作
char ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
putchar(ch);
}
putchar('\n');
if (feof(pf))
printf("end of file\n");
else if (ferror(pf))
printf("error\n");
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
以二进制文件为例:
struct Stu
{
int age;
int weight;
char name[20];
};
int main()
{
//打开文件
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
exit(-1);
}
/*struct Stu s = { 20, 70, "zhangsan" };
fwrite(&s, sizeof(s), 1, pf);
fwrite(&s, sizeof(s), 1, pf);
fwrite(&s, sizeof(s), 1, pf);
fwrite(&s, sizeof(s), 1, pf);
fwrite(&s, sizeof(s), 1, pf);*/
struct Stu s[10];
int ret = fread(s, sizeof(struct Stu), 5, pf);
if (ret == 5)
{
printf("success\n");
for (int i = 0; i < 5; i++)
{
printf("%d %d %s\n", s[i].age, s[i].weight, s[i].name);
}
}
else
{
if (feof(pf))
{
printf("end\n");
}
else if (ferror(pf))
{
printf("err\n");
}
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
8. 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
意思就是你传过去的数据不会第一时间给到文件,而是先给到缓冲区中,然后等到缓冲区内的数据填满了之后再传到文件中。但是你可以手动刷新缓冲区,就可以输出缓冲区的数据到文件中。
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
结束。。