C中的文件 #
C语言具有操作文件的能力,比如打开文件、读取和追加数据、插入和删除数据、关闭文件、删除文件等。
在操作系统中除了常见的文本文件以及二进制文件,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。例如:
- 通常把显示器称为标准输出文件,printf 就是向这个文件输出数据;
- 通常把键盘称为标准输入文件,scanf 就是从这个文件读取数据。
常见硬件设备对应的文件 #
| 文件 | 硬件设备 |
|---|---|
| stdin | 标准输入文件,一般指键盘;scanf()、getchar() 等函数默认从 stdin 获取输入。 |
| stdout | 标准输出文件,一般指显示器;printf()、putchar() 等函数默认向 stdout 输出数据。 |
| stderr | 标准错误文件,一般指显示器;perror() 等函数默认向 stderr 输出数据。 |
| stdprn | 标准打印文件,一般指打印机。 |
操作文件的正确流程为:打开文件 –> 读写文件 –> 关闭文件。文件在进行读写操作之前要先打开,使用完毕要关闭。
所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 FILE 类型的结构体变量中。关闭文件就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。
在C语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读取一整行,还可以读取若干个字节。文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。
文件流 #
所有的文件(保存在磁盘)都要载入内存才能处理,所有的数据必须写入文件(磁盘)才不会丢失。数据在文件和内存之间传递的过程叫做文件流,类似水从一个地方流动到另一个地方。数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。
文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。
输入输出(Input output,IO)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。
我们可以说,打开文件就是打开了一个流。
打开文件 #
- 使用
<stdio.h>头文件中的fopen()函数即可打开文件- 函数原型:
FILE *fopen( const char * filename, const char * mode ); filename为文件名(包括文件路径),mode为打开方式,它们都是字符串。fopen()会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。
- 函数原型:
//以只写方式打开一个当前路径下test.txt文本文件
//fp 通常被称为文件指针。
FILE *fp = fopen("./test.txt","w");
判断是否打开成功 #
打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,我们可以利用这一点来判断文件是否打开成功,
int main(){
FILE *fp = NULL;
if((fp = fopne("./test.txt","w")) == NULL){
printf("File open failed");
}
return 0;
}
打开方式 #
读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)。
整体来说,文件打开方式由 r、w、a、t、b、+ 六个字符拼成,各字符的含义是:
- r(read):读
- w(write):写
- a(append):追加
- t(text):文本文件
- b(binary):二进制文件
- +:读和写
控制读写权限的字符串 #
| 打开方式 | 说明 |
|---|---|
| “r” | 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。 |
| “w” | 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
| “a” | 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
| “r+” | 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。 |
| “w+” | 以“写入/更新”方式打开文件,相当于w和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
| “a+” | 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
控制读写方式的字符串 #
| 打开方式 | 说明 |
|---|---|
| “t” | 文本文件。如果不写,默认为"t"。 |
| “b” | 二进制文件。 |
关闭文件 #
- 文件一旦使用完毕,应该用
fclose()函数把文件关闭,以释放相关资源,避免数据丢失。- 函数原型:
int fclose(FILE *fp); - 文件正常关闭时,fclose() 的返回值为0,如果返回非零值则表示有错误发生。
- 函数原型:
读写文件 #
以字符 #
以字符形式读写文件时,每次可以从文件中读取一个字符,或者向文件中写入一个字符。主要使用两个函数,分别是 fgetc() 和 fputc()。
读 #
fgetc 是 file get char 的缩写,意思是从指定的文件中读取一个字符。
- 原型:
int fgetc (FILE *fp); - fp 为文件指针。fgetc() 读取成功时返回读取到的字符,读取到文件末尾或读取失败时返回
EOF。
#include<stdio.h>
int main(){
//打开文件
FILE *fp = NULL;
if((fp = fopen("./test.txt","r")) == NULL){
printf("File open failed");
return 0;
}
//按照字符读取文件
char c;
while((c = fgetc(fp)) != EOF){
printf("%c",c);
}
//关闭文件
fclose(fp);
return 0;
}
EOF
- 是 end of file 的缩写,表示文件末尾,是在 stdio.h 中定义的宏,它的值是一个负数,往往是 -1。fgetc() 的返回值类型之所以为 int,就是为了容纳这个负数(char不能是负数),EOF 也不全是 -1,也可以是其他负数,这要看编译器的实现。
- EOF 本来表示文件末尾,意味着读取结束,但是很多函数在读取出错时也返回 EOF,那么当返回 EOF 时,到底是文件读取完毕了还是读取出错了?我们可以借助 stdio.h 中的两个函数来判断,分别是 feof() 和 ferror()。
- feof() 函数用来判断文件内部指针是否指向了文件末尾
- 原型:
int feof ( FILE * fp ); - 当指向文件末尾时返回非零值,否则返回零值。
- 原型:
- ferror() 函数用来判断文件操作是否出错
- 原型:
int ferror ( FILE *fp ); - 出错时返回非零值,否则返回零值。
- 原型:
文件位置指针
- 在文件内部有一个位置指针,用来指向当前读写到的位置,也就是读写到第几个字节。在文件打开时,该指针总是指向文件的第一个字节。使用 fgetc() 函数后,该指针会向后移动一个字节,所以可以连续多次使用 fgetc() 读取多个字符。
- 注意:这个文件内部的位置指针与C语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,也就是读写到第几个字节,它不表示地址。文件每读写一次,位置指针就会移动一次,它不需要你在程序中定义和赋值,而是由系统自动设置,对用户是隐藏的。
写 #
fputc 是 file output char 的所以,意思是向指定的文件中写入一个字符。
- 原型:
int fputc ( int ch, FILE *fp ); - ch 为要写入的字符,fp 为文件指针。fputc() 写入成功时返回写入的字符,失败时返回 EOF,返回值类型为 int 也是为了容纳这个负数。
注意:
- 被写入的文件可以用写、读写、追加方式打开,用写或读写方式打开一个已存在的文件时将清除原有的文件内容,并将写入的字符放在文件开头。如需保留原有文件内容,并把写入的字符放在文件末尾,就必须以追加方式打开文件。不管以何种方式打开,被写入的文件若不存在时则创建该文件。
- 每写入一个字符,文件内部位置指针向后移动一个字节。
#include<stdio.h>
int main(){
//打开文件
FILE *fp = NULL;
if((fp = fopen("./test.txt","w")) == NULL){
printf("File open failed");
return 0;
}
//从键盘获取字符,并写入文件,回车结束
char c;
while((c = getchar()) != '\n'){
fputc(c,fp);
}
fclose(fp);
return 0;
}
以字符串 #
fgetc() 和 fputc() 函数每次只能读写一个字符,速度较慢;实际开发中往往是每次读写一个字符串或者一个数据块,这样能明显提高效率,以字符串读主要是用两个函数:fgets、fputs
读 #
fgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组中
- 原型:
char *fgets ( char *str, int n, FILE *fp ); - str 为字符数组,n 为要读取的字符数目,fp 为文件指针。
- 返回值:读取成功时返回字符数组首地址,也即 str;读取失败时返回 NULL;如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL。
注意:
- 读取到的字符串会在末尾自动添加 ‘\0’,n 个字符也包括 ‘\0’。也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。
- 在读取到 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。在C语言中,没有按行读取文件的函数,我们可以借助 fgets(),将 n 的值设置地足够大,每次就可以读取到一行数据。
- fgets() 遇到换行时,会将换行符一并读取到当前字符串。
#include<stdio.h>
#define NUM 100
int main(){
FILE *fp = NULL;
if((fp = fopen("./test.txt","r")) == NULL){
printf("File open failed");
return 0;
}
char c[NUM];
//逐行读取
while(fgets(c,999,fp) != NULL){
printf("%s",c);
}
fclose(fp);
return 0;
}
写 #
fputs() 函数用来向指定的文件写入一个字符串
- 原型:
int fputs( char *str, FILE *fp ); - str 为要写入的字符串,fp 为文件指针。写入成功返回非负数,失败返回 EOF。
#include<stdio.h>
int main(){
FILE *fp = NULL;
if((fp = fopen("./test.txt","w")) == NULL){
printf("File open failed");
return 0;
}
char str[] = "Hello World\nHello C Language";
//写入文件
fputs(str,fp);
fclose(fp);
return 0;
}
以数据块(二进制) #
以二进制读取数据,通常是数组或结构体。如果希望读取多行内容,需要使用 fread() 函数;相应地写入函数为 fwrite()。
读 #
fread() 函数用来从指定文件中读取块数据。所谓块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。
原型:size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );
写 #
fwrite() 函数用来向文件中写入块数据
原型:size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );
说明 #
参数
- ptr 为内存区块的指针,它可以是数组、变量、结构体等。fread() 中的 ptr 用来存放读取到的数据,fwrite() 中的 ptr 用来存放要写入的数据。
- size:表示每个数据块的字节数。
- count:表示要读写的数据块的块数。
- fp:表示文件指针。
- 理论上,每次读写 size*count 个字节的数据。
返回值
- size_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,表示无符号整数,也即非负数,常用来表示数量。
- 返回成功读写的块数,也即 count。如果返回值小于 count:
- 对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
- 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。
对数组进行读写 #
写 #
#include<stdio.h>
int main(){
int age[] = {18,22,55,89};
FILE *fp = NULL;
if((fp = fopen("./age","wb")) == NULL){
printf("File Open Failed");
return 0;
}
//将数组写入文件,每个数据块为sizeof(int)字节,一共sizeof(age) / sizeof(int)个数据块
fwrite(age,sizeof(int),sizeof(age) / sizeof(int),fp);
fclose(fp);
return 0;
}
读 #
#include<stdio.h>
int main(){
FILE *fp = NULL;
if((fp = fopen("./age","rb")) == NULL){
printf("File Open Failed");
return 0;
}
int age[4];
fread(age,sizeof(int),4,fp);
for(int i = 0;i < 4;i++){
printf("%d\n",age[i]);
}
fclose(fp);
return 0;
}
对结构体进行读写 #
写 #
#include<stdio.h>
typedef struct Stu{
int age;
char *name;
}Stu;
int main(){
FILE *fp = NULL;
if((fp = fopen("./stu","wb")) == NULL){
printf("File Open Failed");
return 0;
}
Stu s = {18,"lucy"};
//将一个结构体变量的值写入文件,数据块的大小就是一个结构体的大小,只写入一个数据块
//如果是结构体数组,那么第三个参数就是数组长度
fwrite(&s,sizeof(Stu),1,fp);
fclose(fp);
return 0;
}
读 #
#include<stdio.h>
typedef struct Stu{
int age;
char name[50];
}Stu;
int main(){
FILE *fp = NULL;
if((fp = fopen("./stu","rb")) == NULL){
printf("File Open Failed");
return 0;
}
Stu s;
int i = fread(&s,sizeof(Stu),1,fp);
printf("姓名:%s,年龄:%d",s.name,s.age);
fclose(fp);
return 0;
}
格式化读写文件 #
fscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。
函数原型
int fscanf ( FILE *fp, char * format, ... );
int fprintf ( FILE *fp, char * format, ... );
fprintf() 返回成功写入的字符的个数,失败则返回负数。
fscanf() 返回参数列表中被成功赋值的参数个数。
例如,将Stu结构体变量写入文件中
#include<stdio.h>
typedef struct Stu{
char name[20];
int age;
}Stu;
int main(){
Stu s = {"lucy",18};
//打开文件
FILE *fp;
if((fp = fopen("./stu.txt","w")) == NULL){
printf("File opne failed");
}
//以规定格式写入
fprintf(fp, "%s %d\n",s.name,s.age);
fclose(fp);
return 0;
}
此时,stu.txt文件内容为
lucy 18
然后,按照格式读取就可以了
#include<stdio.h>
typedef struct Stu{
char name[20];
int age;
}Stu;
int main(){
FILE *fp;
if((fp = fopen("./stu.txt","r")) == NULL){
printf("File open failed");
}
Stu s;
//从文件中按照固定格式读取
int i = fscanf(fp,"%s %d\n",&s.name,&s.age);
//打印
printf("name->%s||age->%d",s.name,s.age);
fclose(fp);
return 0;
}
name->lucy||age->18
移动文件位置指针 #
在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写。这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。
实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。
rewind() #
函数原型:void rewind ( FILE *fp );
该函数的作用相当于重置文件位置指针,将指针移到文件开头
fseek() #
函数原型:int fseek ( FILE *fp, long offset, int origin );
- fp 为文件指针,也就是被移动的文件。
- offset 为偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动。
- origin 为起始位置,也就是从何处开始计算偏移量。C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,每个位置都用对应的常量来表示
| 起始点 | 常量名 | 常量值 |
|---|---|---|
| 文件开头 | SEEK_SET | 0 |
| 当前位置 | SEEK_CUR | 1 |
| 文件末尾 | SEEK_END | 2 |
例如,把位置指针移动到离文件开头100个字节处
fseek(fp, 100, 0);
注意:值得说明的是,fseek() 一般用于二进制文件,在文本文件中由于要进行字节和字符转换,计算的位置有时会出错。