本节讲解C中的文件操作。
1.为什么使用文件
我们在运行程序后,运行结果通常不会保留,当下次再运行时,内存中的数据则已被销毁。而使用文件,我们就可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2.文件
2.1程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
2.2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章主要讲数据文件,有时候我们会将数据输出到硬盘,再次需要用的时候再从磁盘上读取数据到内存使用。
2.3.文件名标识
文件名包含3部分:文件路径+文件名主干+文件后缀
如 C:\code\test.txt
3.文件打开/关闭
3.1文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名 FILE 。
不同编译器下,内部实现可能有不同,但大同小异。我们只需学习使用他操作文件。
创建一个FILE结构体指针变量
FILE* pf;
上面让pf指向一个文件的文件信息区,通过这个指针访问的信息区中的信息,就可以访问相关文件。即通过文件指针变量能访问相关联文件。
3.2文件打开/关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
用fopen来打开文件,fclose关闭文件。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
open函数中参数mode的标识
r 只读
如果打开失败,返回NULL。
Return Value
Each of these functions returns a pointer to the open file. A null pointer value indicates an error.
使用例
//打开文件
FILE* fp;
fp = fopen("文件操作.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读取使用
//关闭,置空
fclose(fp);
fp = NULL;
w只写
当文件不存在时,会创建个新文件,同时会覆盖之前的文件。
在这引入一个函数,int fputc( int c, FILE *stream );
往文件信息区结构体stream写入c。
使用例
int main()
{
//打开文件
FILE* fp;
fp = fopen("文件操作.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读取使用
char a = 'a';
for (a; a < 'z';a++) {
fputc(a, fp);
}
//关闭,置空
fclose(fp);
fp = NULL;
return 0;
}
可以看到文件被修改了
a 追加
当以这种方式打开时,读写位置从末尾开始,不会覆盖之前的文件。
4.文件的顺序读写
4.1 对流的简单概述
流是一种高度抽象的概念,可以理解为水流。我们想从多种外部设备读写信息,而每种读写方式各不同,这样的学习成本很高。故封装一个概念,把这些方式,信息都读进流里,而我们只需对流操作,不需对设备的过多的学习,即能操作信息。
比如文件流,即FILE*
还有标准流:(C语言中运行时默认打开)
标准输入流 stdin 键盘
标准输出流 stdout 控制台
标准错误流 stderr 控制台
因为是默认打开的,所以不需要像对文件流操作时一样,需要先打开文件流(fopen)。
上述3个流本质类型还是FILE* 。
下面是从stdin标准输入流获取一个字符,传给ch,并输出到stdout
void test() {
int ch=fgetc(stdin);
printf("%c", ch);
}
结果
v
v
4.2 函数使用
fputc
上面已使用过。
fgetc
以只读方式打开上次的文件,每次读取一个字符,获取成功时,打印到屏幕上。
int main()
{
//打开文件
FILE* fp;
fp = fopen("文件操作.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读取使用
int ch = 0;
while ((ch = fgetc(fp))!= EOF)
{
printf("%c ", ch);
}
//关闭,置空
fclose(fp);
fp = NULL;
return 0;
}
控制台:
a b c d e f g h i j k l m n o p q r s t u v w x y
fputs / fgets
按行写入/读取
//fputs/fgets
void test2() {
//为存储读取的字符准备
char str[255] = { 0 };
//打开文件
FILE* fp;
fp = fopen("文件操作.txt", "w");
if (fp == NULL)
{
perror("fopen");
return ;
}
//fputs使用
fputs("abcde2333333\n", fp);
fputs("xxxxxde2333333", fp);
//此时如果直接gets会从上次的位置往后读,结果为大量"屯"
//需要关闭文件再打开
fclose(fp);
fp = NULL;
fp = fopen("文件操作.txt", "r");
//fgets使用
fgets(str, 244, fp);
printf("%s", str);
//关闭,置空
fclose(fp);
fp = NULL;
}
结果
abcde2333333
fscanf / fprintf
int fscanf( FILE *stream, const char *format [, argument ]... ):
scanf("XXX")函数实际上就是将字符串内容输入到stdin上,而fscanf就是将内容输入到stream里。
int fprintf( FILE *stream, const char *format [, argument ]...):
printf("XXX")函数实际上就是将字符串内容输出到stdout上,而fprintf就是将内容输出到stream里。
使用例
// fscanf fprintf
void test3() {
char str[256] = { 0 };
//打开文件
FILE* fp;
fp = fopen("文件操作.txt", "w");
if (fp == NULL)
{
perror("fopen");
return;
}
//写入(输出)
fprintf(fp, "Success\n This.");
//fscanf(fp, "Success\n This2.");
fclose(fp);
fp = NULL;
fp = fopen("文件操作.txt", "r");
//从fp流 读取字符串到str
fscanf(fp,"%s",str);
//输出到控制台
fprintf(stdout, str);
fclose(fp);
fp = NULL;
}
结果
控制台
Success
fread / fwrite
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
可以把任何数据buffer以二进制的格式写进文件,然后可以用二进制的方式从流读取出来到buffer。
// fread fwrite
void test4() {
float a = 2.333333;
FILE* fp;
fp = fopen("文件操作.txt", "w");
if (fp == NULL)
{
perror("fopen");
return;
}
fwrite(&a, sizeof(a), 1, fp);
fclose(fp);
fp = NULL;
fp = fopen("文件操作.txt", "r");
float new_a = 0;
fread(&new_a, sizeof(new_a), 1, fp);
printf("%f", new_a);
}
结果,在txt中以文本形式打开没法正常看
以二进制形式打开存到对应类型变量中,按常用格式输出到控制台就可读。
2.333333
fwrite写进去的还可以是,结构体,其他类型变量等等。要注意,用什么方式写进去,就用什么方式读取出来。
总结
读取函数从流中以某种方式 读取字符/字符串常量,到某个字符/字符串变量中。
写入函数把字符/字符串变量的值,写进指定流中。
5. 文件的随机读写
5.1 fseek
根据文件指针的位置和偏移量来定位文件指针。
int fseek( FILE *stream, long offset, int origin );
参数origin的取值
SEEK_CUR 当前位置
Current position of file pointer
SEEK_END 结尾位置
End of file
SEEK_SET 开始位置
Beginning of file
/* fseek example */
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
/***************************************/
fseek ( pFile , 9 , SEEK_SET );
/***************************************/
fputs ( " sam" , pFile );
fclose ( pFile );
return 0;
}
光标移动到,从开始位置,往后偏移9的位置。再用puts把 "n ap"改成" sam"。
可以用负数访问往前偏移的位置;SEEK_END-1才是最后一个元素(字)位置。
5.2 ftell
返回文件指针相对于起始位置的偏移量。
long ftell( FILE *stream );
能得到当前光标位置,距离起始位置的偏移量。
5.3 rewind
回到起始位置。
void rewind( FILE *stream );
6. 文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
数字10000,以ASCⅡ形式存进文件,每个数字占一个字节,总共占5个字节。如果以二进制形式存放,则占一个整形即4个字节。
7. 文件读取结束的判定
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数
ferror
Return Value
If no error has occurred on stream, ferror returns 0. Otherwise, it returns a nonzero value.
没有错误返回0,有错返回非0。
feof
Return Value
The feof function returns a nonzero value after the first read operation that attempts to read past the end of the file. It returns 0 if the current position is not end of file. There is no error return.
读到结尾返回非0,没有读到最后返回0。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if(!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
8. 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
简单来说
之前提到的各种写入函数,其实并非是直接将数据写进硬盘文件,而是在执行完函数后先写进缓冲区了,但,为什么最后还是写进了硬盘呢?这是因为fclose在关闭文件的同时也把缓冲区的数据写进了文件。
现在有一个新的函数,能实现直接把缓冲区数据写进硬盘。
fflush
Flushes a stream. 刷新一个流
int fflush( FILE *stream );
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
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;
}
上面代码,在第一次睡眠时,可以看到文本文件是空的;第二次睡眠时,文件已经写入数据。同时,因为在sleep期间就写入,证明不是fclose的作用,而是fflush将数据写进硬盘。
睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容
睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容
刷新缓冲区
再睡眠10秒-此时,再次打开test.txt文件,文件有内容了
10秒刷新缓冲区后才有字