1. 为什么使用文件
在之前我们使用的无论是结构体还是数组,每次程序运行完后数据就会被清空。如果我们想要在下一次运行程序继续使用之前的值时,就需要重新输入一遍数据,这样的操作很不方便。
如果我们想将数据保存下来,只有当我们自己选择删除数据时,数据才会被清除,这时就可以使用文件。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2. 什么是文件
磁盘上的文件是文件。
在C程序设计中,文件一般指的是数据文件和程序文件(从文件功能上分类)
2.1 数据文件
程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件
2.2 程序文件
包括源程序文件(后缀为 .c ) , 目标文件( windows 环境后缀为 .obj ) , 可执行程序( windows 环境后缀为.exe)。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
路径:c:\code\
文件名主干:test
文件后缀: .txt
为了方便起见,文件标识常被称为文件名
3. 文件的打开和关闭
3.1 文件类型指针
FILE * fp;
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
缓冲文件系统为每个被使用的文件在内存中开辟一个相应的缓冲区(文件信息区),用来存放文件的有关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是在stdio.h中定义的,取名FILE。
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。 每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息, 使用者不必关心细节。
上述定义中,f p 是一个指向 FILE 类型数据的指针变量。可以使f p 指向某个文件的文件信息区。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联 的文件
3.2 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
(因为操作系统对于同时打开的文件数目是有限制的,所以在文件使用结束后必须使用fclose关闭文件,否则会出现意想不到的错误)
3.2.1 文件的打开
可以使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象。下面是这个函数调用的原型:
FILE * fopen ( const char * filename, const char * mode );
//fopen(文件路径,文件使用方式);
filename 是字符串,用来命名文件,fopen函数打开filename指定的文件,文件使用方式mode 的值可以是下列值中的一个:
注:fopen 返回一个文件信息区的地址,如果打开失败会返回一个空指针
3.2.2 文件的关闭
为了关闭文件,应使用 fclose( ) 函数。
int fclose ( FILE * stream );
如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。
3.2.3 示例
#include <stdio.h>int main (){FILE * pf ;// 打开文件pf = fopen ( "myfile.txt" , "w" );// 文件操作if ( pf != NULL ){/*一系列操作*///关闭文件fclose ( pf );pf = NULL;}return 0 ;}
4. 文件的顺序读写
4.1 fgetc
int fgetc(FILE * stream);
函数功能:从一个以只读或读写方式打开的文件上读字符, 从 stream指定的 流或文件 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
-
如果读取成功,则返回该字符。
以无符号 char 强制转换为 int 的形式返回读取的字符。 -
如果到达文件末尾或发生读错误,则返回 EOF。
#include <stdio.h>
int main (){FILE * pf ;// 打开文件pf = fopen ( "myfile.txt" , "w" );// 文件操作int c = 0;int n = 10;if ( pf != NULL ){do
{
c = fgetc(fp);
printf("%c", c);
} while (--n);//关闭文件fclose ( pf );pf = NULL;}return 0 ;}
4.2 fputc
int fputc(int char, FILE* stream);
函数功能:该函数的功能是把参数 char 指定的字符(一个无符号字符)写入到 stream 指定的流或文件中,并把位置标识符往前移动。
- 如果没有发生错误,则返回被写入的字符。
- 如果发生错误,则返回 EOF。
#include <stdio.h>
int main (){FILE * pf ;// 打开文件pf = fopen ( "myfile.txt" , "r" );// 文件操作int c = 0;int n = 10;while (--n){c = fputc(fp);
printf("%c", c);}//关闭文件fclose ( pf );pf = NULL;return 0 ;}
4.3 fgets
char* fgets(char* str, int n, FILE* stream);
函数功能:该函数从 stream指定的流或文件 中读取字符串并在字符串末尾添加‘\0’,然后存入s,最多读n-1个字符,当读到回车换行符、到达文件尾或读满n-1个字符时,就停止读取。把它存储在 str 所指向的字符串内。
- 读取成功,函数返回该字符串的首地址,即指针 str 的值
- 如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。
- 读取失败返回空指针NULL
- 与gets()不同的是,fgets()从指定的流读取一行字符串,会将字符串和换行符'\n'一起读走。
#include <stdio.h>
int main (){FILE * pf ;char str[60];// 打开文件pf = fopen ( "myfile.txt" , "r" );if (pf == NULL){
perror("打开文件时发生错误");
return(-1);
}//文件操作if (fgets(str, 60, stdin) != NULL){
//从标准输入流(键盘)向str中写入内容
puts(str);
}//关闭文件fclose ( pf );pf = NULL;return 0 ;}
4.4 fputs
int fputs(const char* str, FILE* stream);
函数功能:该函数把字符串写入到 stream指定的流或文件中,但不包括空字符。
- 写入成功,该函数返回一个非负值
- 如果发生错误则返回 EOF。
- 与puts()不同的是,fputs()不会在写入文件的字符串末尾加上换行符'\n'
#include <stdio.h>
int main (){FILE * pf ;char str[10] = "abcdef";// 打开文件pf = fopen ( "myfile.txt" , "w" );if (pf == NULL){
perror("打开文件时发生错误");
return(-1);
}//文件操作fputs(str, pf); //从str中向 pf 文件写入内容//关闭文件fclose ( pf );pf = NULL;return 0 ;}
4.5 fscanf
int fscanf(FILE* stream, const char* format, ...);
函数功能:从 stream指定的流或文件 读取格式化输入
- 如果成功,该函数返回成功匹配和赋值的个数。
- 如果到达文件末尾或发生读错误,则返回 EOF。
- 后两个参数与函数scanf()的参数相同
#include <stdio.h>
#include <stdlib.h>
int main (){FILE * pf ;char str1[10] = "abcdef";char str2[10];// 打开文件pf = fopen ( "myfile.txt" , "w+" );if (pf == NULL){
perror("打开文件时发生错误");
return(-1);
}//文件操作fputs(str1, pf); //从str1中向 pf 文件写入内容rewind(pf);fscanf(pf, "%s" , str2); //pf文件中按格式读取内容,放到str2中puts(str2);//关闭文件fclose ( pf );pf = NULL;return 0 ;}
4.6 fprintf
int fprintf(FILE* stream, const char* format, ...);
函数功能:发送格式化输出到 stream指定的流或文件中。
- 如果成功,则返回写入的字符总数,否则返回一个负数。
- 后两个参数和返回值与函数printf()相同
#include <stdio.h>
#include <stdlib.h>
int main (){FILE * pf ;char str1[10] = "abcdef";char str2[10];// 打开文件pf = fopen ( "myfile.txt" , "w+" );if (pf == NULL){
perror("打开文件时发生错误");
return(-1);
}//文件操作fprintf(pf, "%s" , str1); //从str1中输出内容到 pf 文件rewind(pf);fscanf(pf, "%s" , str2); //pf文件中按格式读取内容,放到str2中puts(str2);//关闭文件fclose ( pf );pf = NULL;return 0 ;}
4.7 fread
size_t fread(void* ptr, size_t size, size_t count, FILE * stream);
函数功能:从 stream指定的流或文件 读取数据块到 ptr 所指向的内存中
- 读取成功,返回的是实际读到的数据块个数
- 如果发生错误可能是读到了文件末尾,也可能是读取发生了错误。
- ptr是待读入数据块存储的起始地址
- size是每个数据块的大小(待读入的每个数据块的字节数)
- count是最多允许读取的数据块个数(每个数据块size个字节)
- 如果size或count中有一个为零,函数返回零,流状态和ptr指向的内容都保持不变。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main (){FILE * pf ;char str1[]= "I am a student";
char str2[20];// 打开文件pf = fopen ( "myfile.txt" , "rb+" );if (pf == NULL){
perror("打开文件时发生错误");
return(-1);
}//文件操作fputs(str1, pf);//从str1中输出内容到 pf 文件
rewind(pf);
fread(str2, strlen(str1) + 1,1,pf);//pf文件中按格式读取内容,放到str2中
printf("%s\n", str2);//关闭文件fclose ( pf );pf = NULL;return 0 ;}
4.8 fwrite
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* fp);
函数功能:该函数把ptr指向的内存中的数据块写入到 stream指定的流或文件中
- 写入成功,函数返回的是实际写入的数据块个数
- 如果该数字与 count 参数不同,则会显示一个错误
- ptr是待输出数据块的起始地址
- size是每个数据块的大小(待输出的每个数据块的字节数)
- count是最多允许写入的数据块个数(每个数据块size个字节)
- 用户指定的内存块大小,最小为1字节,最大为整个文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main (){FILE * pf ;char str1[]= "I am a student";
char str2[20];// 打开文件pf = fopen ( "myfile.txt" , "rb+" );if (pf == NULL){
perror("打开文件时发生错误");
return(-1);
}//文件操作fwrite(str1, strlen(str1) + 1, 1, fp);//从str1中输出内容到 pf 文件
rewind(pf);
fread(str2, strlen(str1) + 1,1,pf);//pf文件中按格式读取内容,放到str2中
printf("%s\n", str2);//关闭文件fclose ( pf );pf = NULL;return 0 ;}
5. 文件的随机读写
前面介绍的文件读写函数都是顺序读写,从文件头开始,依次读取各个数据,但有时需要读取文件中间部位的数据,这时从头开始读取就很不方便。C语言提供了一个方法解决这个问题,即移动文件内部的指针,再进行读写。
实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。
5.1文件定位函数
5.1.1 rewind
void rewind(FILE* stream);
函数功能:将文件位置指针指向文件首字节,即重置位置指针到文件首部
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char str[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
rewind(pFile);
fread(str, 1, 26, pFile);
fclose(pFile);
str[26] = '\0';
puts(str);
return 0;
}
5.1.2 fseek
int fseek(FILE* stream, long int offset, int whence);
函数功能:将文件位置指针从stream开始移动offset个字节指示下一个要读取的数据的位置
- offset 是一个偏移量,long int类型,它告诉文件位置指针要跳过多少字节,offset为正时,向后移动,为负时,向前移动
- whence 为起始位置,也就是从何处开始计算偏移量,C语言规定的起始位置有三种,如下:
SEEK_CUR - 文件指针当前的位置SEEK_END - 文件末尾的位置SEEK_SET - 文件开始位置
- 如果成功,则该函数返回零,否则返回非零值
- 一般用于二进制文件,文本格式进行转换时,计算的位置可能发生改变
#include <stdio.h>int main (){FILE * pFile ;pFile = fopen ( "myfile.txt" , "wb" );fputs ( "This is an apple." , pFile );fseek ( pFile , 9 , SEEK_SET );fputs ( " sam" , pFile );fclose ( pFile );return 0 ;}
5.1.3 ftell
long int ftell(FILE* stream);
函数功能:返回文件指针相对于文件起始位置的字节偏移量
- 调用成功,返回文件指针相对于文件起始位置的字节偏移量
- 如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。
#include <stdio.h>int main (){FILE * pFile ;long size ;pFile = fopen ( "myfile.txt" , "rb" );if ( pFile == NULL ) perror ( "fopen" );else{fseek ( pFile , 0 , SEEK_END );size = ftell ( pFile );fclose ( pFile );printf ( "Size of myfile.txt: %ld bytes.\n" , size );}return 0 ;}
6. 文本文件和二进制文件
前面讲到:C程序按照文件的功能将文件分成数据文件和程序文件。
而C程序又按照数据的组织形式把文件分为ASCII文件和二进制文件。
- 二进制文件,即数据在内存中以二进制的形式存储,不加转换的直接输出到外存。
- ASCII文件,又称文本文件,即数据在存储前,需要转换成ASCII字符形式的文件。
在文本文件中,数值型数据的每一位数字作为一个字符以其ASCII码的形式存储,因此,文本文件中的每一位数字都单独占用一个字节的存储空间。
二进制文件则是把整个数字作为一个二进制数存储的,数值的每一位数字不占用单独的存储空间。
7. 文件读取结束的判定
7.1 为什么要对文件读取结束进行判定
有两种方式可能造成文件读取结束:
一种是文件读取到末尾而结束,另一种是读取发生错误(读取失败)而结束。
所以读取结束时,需要判断是读取失败结束,还是读取到文件末尾而结束。
7.2 判断文件读取是否结束
- 文本文件读取是否结束,判断返回值是否为 EOF ( 返回值是数值 ),或者 NULL (返回值是指针),是则读取结束。
例如: fgetc 判断是否为 EOF ,fgets 判断返回值是否为 NULL 。- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如: fread判断返回值是否小于实际要读的个数。
7.3 文件检测函数
7.3.1 ferror
int ferror(FILE* stream);
函数功能:测试给定流 stream 的错误标识符。如果ferror返回值为0(假),表示读取文件未出错。如果返回一个非零值,表示读取文件时发生错误。
- 对同一个文件 每一次调用输入输出函数,均产生一个新的ferror函数值。
- 因此,应当在调用一个输入输出函数后立即检查ferror函数的值,否则信息会丢失。
- 在执行fopen函数时,ferror函数的初始值自动置为0
7.3.2 feof
int feof(FILE* stream);
函数功能:feof函数是在文件读取结束后,判断文件读取结束的原因的,是读取失败结束,还是遇到文件尾结束。
-
在文件读取过程中,不能用 feof函数的返回值直接用来判断文件的是否结束。
-
如果文件结束,则返回非0值,否则返回0。
-
读取文本判断是否结束时,fgetc看返回值是否为EOF, fgets看返回值是否为NULL
-
二进制文件判断读取结束,看实际读取个数是否小于要求读取个数
-
文件读取结束,通常需要 ferror 和 feof 联合使用( 判断文件读取结束的时候,是读取失败结束,还是遇到文件尾结束)
#include <stdio.h>#include <stdlib.h>int main ( void ){int c ;FILE * pf = fopen ( "myfile.txt" , "r" );if ( ! pf ) {perror ( "fopen" );return EXIT_FAILURE ;}//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回 EOFwhile (( c = fgetc ( pf )) != EOF ) // 标准 C I/O 读取文件循环{putchar ( c );}// 判断是什么原因结束的if ( ferror ( pf ))puts ( "I/O error when reading" );else if ( feof ( pf ))puts ( "End of file reached successfully" );fclose ( pf );}
#include <stdio.h>
enum { SIZE = 5 };int main ( void ){double a [ SIZE ] = { 1. , 2. , 3. , 4. , 5. };FILE * fp = fopen ( "myfile.txt" , "wb" ); // 必须用二进制模式fwrite ( a , sizeof * a , SIZE , fp ); // 写 double 的数组fclose ( fp );double b [ SIZE ];fp = fopen ( "myfile.txt" , "rb" );size_t ret_code = fread ( b , sizeof * b , SIZE , fp ); // 读 double 的数组if ( ret_code == SIZE ) {puts ( "Array read successfully, contents: " );for ( int n = 0 ; n < SIZE ; ++ n )printf ( "%.2f " , b [ n ]);putchar ( '\n' );} else { // error handlingif ( feof ( fp )) //判断是否是读到文件末尾而结printf ( "Error reading test.bin: unexpected end of file\n" );else if ( ferror ( fp )) { //判断是否是读取错误而结束perror ( "Error reading test.bin" );}}fclose ( fp );}
8. 文件缓冲区
缓冲文件系统是指:C语言为了提高数据的输入/输出的速度,在缓冲文件系统中,给正在使用的每一个文件建立一个“文件缓冲区”。
- 如果从内存向磁盘输出数据会先从程序数据区送到内存中的输出缓冲区,装满缓冲区后才一起送到磁盘上。
- 如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存的输入缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
- 缓冲区的大小根据C编译系统决定的。
建立文件缓冲区虽然可以提高I/O的性能,但也有一些副作用,例如在缓冲区内容还未写入磁盘时,计算机突然死机或掉电,数据就会丢失,永远也找不回来,再如缓冲区被写入无用的数据时,如果不清除,其后的文件读操作都首先要读取这些无用的数据。