目录
一.文件知识基础
1. 为什么使用文件
2. 什么是文件
2.1文件分类
2.2 文件名
3.C语言文件指针的理解
4.EOF
二.文件的打开与关闭
1.文件打开
2.文件关闭
3.联合使用
三.文件读写操作
1.字符读写
2.文本行读写
3.数据块读写
四.文件指针的变动与随机读写
1.fseek
2.ftell
3.rewind
五.文件读取结束的判断
0.feof
1.字符读写
2.数据行读写
3.数据块读写
六.文件缓冲区
程序的如果数据是存放在内存中,程序退出的时候,数据自然就不存在了,如果需要把信息记录下来,就需要使得数据持久化保存下一次可以直接读取并使用。
使得数据持久化可以把数据存放在磁盘文件或者存放到数据库。
计算机可以在各种存储介质(磁盘、磁带和光盘等)上存储信息。计算机操作系统给用户提供了信息存储的统一逻辑视图。这样的最小存储单位就是文件。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
(1)在程序设计中,可以以功能将文件分为两种:程序文件、数据文件。
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。 数据文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件。
(2) 文件也可以根据数据组织形式分为图片文件,语音文件等。
这些文件他们的扩展名(用来表示某种文件格式所采用的机制)也不同。不同的文件要求不同的软件打开。
(3)而在C语言里一般将文件分为:
文本文件(存放字符/文字,打开,存放和识别内容的方式都通过ASCII码,后缀名.txt)
二进制文件(直接存放二进制的内容,一串01,后缀名.data)
例如:十进制10存入不同文件
一个文件要有一个唯一的文件标识,以便用户识别和引用。 文件名包含3部分:
文件路径+文件名主干+文件后缀
如: c:\code\test.txt 为了方便起见,文件标识常被称为文件名。
在C语言里对文件进行一系列操作是需要借助一个工具的,这个工具就是C语言文件指针。因为在对文件进行读写操作时需要记录这时候文件写到哪里了还有当前文件在哪个位置等很多信息。所以对于每个被使用的文件系统都会在内存中开辟一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息通过保存在一个结构体变量中,该结构体类型是有系统声明的,取名FILE。在打开文件时系统会创建一个FILE并填充其中的信息, 使用者不必关心细节。
对文件进行读,写,追加等操作都是需要通过在对应操作函数里传入FILE里的信息作为参数来完成的。
又由于FILE指针可以更加方便管理FILE得到这个信息区的信息。 所以一般我们都创建一个FILE*的指针变量来接收打开文件这个函数返回的信息区地址,然后进行后续文件操作。
FILE* pf = fopen("c:\\code\\word.txt","w");
EOF全称:end of file,本质是整型-1。
在C语言中表示文件结束符。在文本文件中,数据都是以字符的ASCII代码值的形式存放,范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。C语言中,EOF常被作为文件结束的标志。一般作为返回值,判断是否遇到文件末尾或者调用一个函数是否成功。
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。打开文件的同时,也要接收返回的FILE*指针变量建立指针和文件的关系,最后通过FILE*指针变量关闭文件。
对于fopen函数,第一个参数为文件名第二个参数为 打开方式,一般通过传入字符串来实现。打开成功返回FILE*,否则返回空指针。
第一个参数有两种方式:
"c:\\code\\word.txt" //第一种,传入完整的路径,注意此时\\里的第一个\是转义字符
"world.txt" //第二种,如果文件路径就在工厂文件的当前路径,则省略前面的路径
第二参数具体见下表:
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
对于关闭文件,只需要传入当时打开文件创建的那个FILE*指针即可。关闭成功返回0,否则返回EOF。
注意要判断是否打开成功!然后再使用。
#include
int main ()
{
//打开文件
FILE *pFile = fopen ("myfile.txt","w");
//判断是否打开成功
if (pFile!=NULL)
{
//……文件操作
//fputs ("fopen example",pFile);
}
//关闭文件
fclose (pFile);
return 0;
}
整理归纳,可以将读写函数分为三类,可以一个一个字符读写或者一行一行的读写,或者以数据块的形式一次读写一整块。其中对于二进制文件来说一般使用第三种,也可以使用前两种但是会有点小问题需要解决,具体参考第五部分文件读取结束的判断。
fgetc和fputc
FILE*指向的信息区里面会有位置指示器记录文件读取到的位置,如果这个文件你读到第二十字符的位置,这个位置指示器会记录那个位置。
对于fgetc,如果位置指示器不是指向文件末尾,它就会返回当前位置指示器指向位置的那个字符 ,并将位置指示器移到下一个位置。如果该位置指示器指向文件末尾,将返回EOF,位置指示器位置不变,并设置文件尾指示符(feof函数),记录因为遇到文件末尾读写结束 。
char c = fgetc (pFile);
第一个参数为要写入文件的参数,第二个为目标文件的FILE*指针。写入成功后,将返回所写字符。如果发生写入错误,则返回 EOF 并设置错误示器(ferror,文件操作相关函数,下面具体介绍)。
fputc ( 'a', pFile );
ferror
传入要检查是否发生错误的文件指针,如果对该文件操作过程中发生读写错误则类似上面的fputc会主动设置与该文件流关联的错误指示器,这时ferror函数会返回非零值。否则,将返回零。
此指标通常由失败的流上的先前操作设置,并通过调用清除错误、倒带或重复打开来清除。
fgets和fputs
实现从该文件流中读取一串字符并将其作为 C 字符串存储到 str 中。num表明能读的最大个数,什么停止呢?
1.读取 了(num-1) 个字符了 2.读到了换行符'\n'或EOF文件结尾
上面一种情况发生了 fgets就 停止读取,如果是2遇到了换行符使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。如果是1读满了,终止空字符会自动追加到复制到 str 的字符之后,这也是为什么只能读num-1个字符,因为还有'\0'需要存放。读取成功后,位置指示器会对应移动。
如果读取成功后,该函数返回 str。如果在尝试读取字符过程中时遇到文件结尾,则会设置 eof 指示符 (feof,具体参见第五部分的讲解)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。如果发生读取错误,则设置错误指示器 (ferror) 并返回空指针(但 str 所指向的内容可能已更改)。
char mystring [100];
fgets (mystring , 100 , pFile);
将 str 所指向的 C 字符串写入流。该函数从指定的地址 (str) 开始复制,直到达到终止空字符 ('0')。此终止空字符不会被复制到流中。
成功写入后,函数将返回非负值。出错时,该函数返回 EOF 并设置错误指示器(ferror)。
char sentence [256]="Enter sentence to append";
fputs (sentence,pFile);
fgets每次最多只能从文件中读取一行内容,因为 fgets() 遇到换行符就结束读取。如果希望读取多行内容,需要使用 fread, fwrite。
fread和fwrite
从文件流中读取一个数据块的内容(字节大小为每个元素大小乘元素个数即size*count),并将它们存储在 ptr 指定的内存块中。流的位置指示器按读取的字节总数前进。如果大小或数量为零,则该函数返回零,并且 不做其他操作,ptr 所指向的流状态和内容保持不变。
函数会返回读取的总字节数。如果读取了巧好一个数据块的大小,说明读取很成功,如果没读满则有两种情况:
1.遇到EOF ,会设置错误指示器( feof ) 2.遇到读写错误,会设置错误指示器(ferror)
int num=0;
char buffer[100]={0};
num = fread (buffer,1,sizeof(buffer),pFile);
写入一个数据块(字节大小为每个元素大小乘元素个数即size*count),内存块地址从 ptr开始。流的位置指示器按写入的总字节数前进。如果大小或数量为零,则该函数返回零,并且 不做其他操作,ptr 所指向的流状态和内容保持不变。
函数会返回写入的总字节数。如果写入了巧好一个数据块的大小,说明写入很成功,如果没写满则有两种情况:
1.遇到EOF ,会设置错误指示器( feof ) 2.遇到读写错误,会设置错误指示器(ferror)
char buffer[] = { 'x' , 'y' , 'z' };
pFile = fopen ("myfile.bin", "wb");
fwrite (buffer , sizeof(char), sizeof(buffer), pFile);
上文提到的位置指示器其实是可以通过函数随意变动的,从而实现随机读写。比如当你读到第四行时想随机读第四个字符,你可以调整位置指示器移动到需要读写的位置再进行读写,这通常也被称为文件的定位。
将与流关联的位置指示器设置为新位置。第一个参数 为文件指针;第二个参数 offset 为偏移量,它表示要移动的字节数,整数表示正向偏移,负数表示负向偏移;第三个参数 from 表示设定从文件的哪里开始偏移,具体见下表。
Constant | Reference position |
---|---|
SEEK_SET | Beginning of file |
SEEK_CUR | Current position of the file pointer |
SEEK_END | End of file * |
1.对于以二进制模式打开的流,新位置是通过向原点指定的参考位置添加偏移量来定义的。
2.对于以文本模式打开的流,偏移量应为零或上一次调用 ftell函数 返回的值,并且原点必须SEEK_SET。
如果成功,该函数返回零。否则,它将返回非零值。如果发生读取或写入错误,则设置错误指示器(ferror)。
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
fseek ( pFile , 9 , SEEK_SET );//
注:fseek 函数一般用于二进制文件。需要特别注意,当 fseek 函数用于文本文件操作时,一定要注意回车换行的情况。因为在一般浏览工具中,回车换行被视为两个字符,但文件读写和定位却按照一个字符 进行处理。
返回流的位置指示器的当前值。对于二进制流,这是从文件开头开始的字节数。对于文本流,数值可能没有意义,但仍可用于稍后使用 fseek 将位置恢复到同一位置( fseek( fp, ftell(pf),SEEK_SET) )。
如果函数成功,返回位置指示器的当前值。失败时,返回 -1L,并将 errno 设置为系统特定的正值(这样,程序员可以使用perror打印错误信息)。
fseek (pFile, 0, SEEK_END);
int size=ftell (pFile);
将流的位置指示器设置为文件的开头。成功调用此函数后,将清除与流关联的文件结束和错误内部指示器,并删除以前调用此流上的读写错误的所有影响。
rewind (pFile);
真实的读写文件我们不可能只读一个字符或者一行数据,我们会利用循环来根据是否到达文件末尾来判断是否继续读写。
检查是否有因为读写遇到了文件结尾而结束的情况。如果是,则返回与零不同的值。函数通过查看文件末尾指示器来实现功能,该指示器通常由尝试在文件末尾或文件末尾读取的流上的先前操作设置记录。该指标通过调用清除器、倒带、待定、启动或 freopen 来清除。尽管如果此类调用未重新定位位置指示器,则下一个 i/o 操作可能会再次设置该指示器。
通俗来说,文件末尾指示器和文件位置指示器不一样。 一个标记文件读写的当前位置,一个类似flag,读写函数当因为遇到了文件结尾而结束时会改变flag,feof只是返回了该flag的值。
注意:不能单纯利用这个函数判断是否到达文件结尾,因为要判断是否到达了文件末尾要看文件位置指示器是否在文件末尾。当读完最后一个数据,文件位置指示器自动移动已经到达了末尾但不进行读取时,fgetc等函数还不会设置feof的值。ferror和feof很像都是类似一个flag的功能,当遇到读写错误时,flag被更改。所以feof和ferror应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
那么,如何判断我们已经到达文件末尾不需要再读数据了?这需要具体根据对应的函数返回值来判断是否继续读写。对应我们也分为三种介绍:字符读写、数据行的读写,数据块的读写。
判断条件为是否是 EOF
//统计全文字符x的个数 ,并将不是a的字符写入另外文件
FILE * pFile1;
pFile1=fopen ("read.txt","r");
FILE * pFile2;
pFile2 = fopen ("write.txt","w");
if (pFile1==NULL||pFile2==NULL)
perror ("Error opening file");
else
{
int c;
int n = 0;
do
{
c = fgetc (pFile1);
if (c == 'x')
n++;
else
fputc ( c , pFile );
} while (c != EOF);
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose (pFile1);
fclose (pFile2);
}
二进制文件来说一般不这样使用,而使用fread和fwrite,正是因为此。二进制是直接将内存里的数据写入文件,那么有可能某个字节的内容巧好为-1,这时候会误判为到达文件末尾出现错误。所以这时候就需要调用feof查看是否读写时到达了文件结尾而导致读写结束来解决这个问题。
判断返回值是否为NULL
//拷贝文件1到文件2,一次一个数组
FILE * pFile1;
FILE * pFile2;
pFile1 = fopen ("myfile1.txt" , "r");
pFile2 = fopen ("myfile2.txt" , "w");
if (pFile1 == NULL||pFile2==)
perror ("Error opening file");
else
{
char s [100];
char* c=NULL;
while(c=fgets (s , 100 , pFile))
{
fputs (s,pFile);
}
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
}
fclose (pFile1);
fclose (pFile2);
因为一次性读num个size大小的数据,所以使用 fread() 和 fwrite() 时应该以二进制的形式打开。判断返回值与期望num是否相同。
int size=100;
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp2 = fopen("write.bin", "wb"); // 必须用二进制模式
FILE *fp1 = fopen("test.bin","rb");
double b[SIZE];
size_t ret_code=fread(b, sizeof *b, SIZE, fp1);
while( ret_code== SIZE)
{
fwrite(b,sizeof *b, SIZE, fp2);
ret_code=fread(b, sizeof *b, SIZE, fp1);
}
// error handling
if (feof(fp1))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp1))
perror("Error reading test.bin");
fclose(fp1);
fclose(fp2);
事实上,文件并不是调用一次fputc就会写入一个字符进文件,系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区或者程序员调用指令让缓冲区的数据写入文件才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
由于CPU 与 I/O 设备间速度不匹配。为了缓和 CPU 与 I/O 设备之间速度不匹配矛盾。使用文件缓冲区可减少读取硬盘的次数。文件缓冲区是用以暂时存放读写期间的文件数据而在内存区预留的一定空间。
fflush(pf);//刷新缓冲区时,可以不必等到缓冲区满就可以将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
注意:文件关闭操作会自动刷新缓冲区。因为有缓冲区的存在,C语言在操作文件的时候,需要刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,没有关闭文件或者刷新文件将导致数据没被写入文件而丢失。