C语言——文件操作

mljy. 2024-06-17 08:05:04 阅读 64

在之前我们写的程序中当程序结束,内存就被回收数据就丢失了,那么在计算机中的那些需要保存写下的数据时,只把数据写到内存当中就无法一直保留,如果要将数据进行持久化的保存这时就需要再将数据传输到磁盘(硬盘)的文件上。这本篇中我们就来了解文件是什么、有哪些类型的文件,以及学习在程序中实现文件的读和写,还有实现读和写相关的函数,相信看完本篇的讲解能对文件有一定的认识,加油吧!!!


1.什么是文件

磁盘(硬盘)上的文件是文件。

但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

1.1 程序文件

程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程(windows环境后缀为.exe)

1.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

在以前各篇所处理数据的输入输出都是以终端为对象的,即从终端的键盘输⼊数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件

2.文件名

⼀个文件要有⼀个唯一的文件标识,以便用户识别和引用

文件名包含3个部分

1.文件路径 2.文件主干名 3.文件后缀

例如:C:\Users\zhuohongze\Desktop\c-language\test_6_10(1).txt

在以上的文件名中也是由三部分组成的

3.二进制文件和文本文件 

根据数据的组织形式,数据文件被称为文本文件或者二进制文件

数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是文本文件

⼀个数据在文件中是怎么存储的呢?

字符⼀律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用⼆进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而二进制形式输出,则在磁盘上只占4个字节。

 在二进制文件的读和写将在以下的了解fwrite和fread函数时进行细致的讲解

4.文件的打开与关闭

4.1 流和标准流

4.1.1流

在学习文件的打开关闭前先要来了解流,在我们将数据传到外部设备时或者要读取外部设备的数据,外部设备可能是光盘也可能是硬盘上的文件等等,但是不同的外部设备的输入和输出方式可能不同,这时我们要操作外部设备方法都不同,如果给每个外部设备都写一个操作方法就会很繁琐,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。

有了流程序员就不需要了解外部设备是怎么操作的,只需要关注怎么样操作流就可以了,而流怎么把数据给外部设备这种底层的东西就不需要关注了

C程序针对文件、画⾯、键盘等的数据输入输出操作都是通过流操作的。

⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要先打开流,然后操作。 

4.1.2 标准流

那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?

那是因为C语⾔程序在启动的时候,默认打开了3个流:

• stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。

• stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。

• stderr - 标准错误流,大多数环境中输出到显示器界面。

编写时程序默认打开了这三个流,所以我们使用scanf、printf等函数就可以直接进行输入输出操作的。

4.2 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

每个被使用的文件都在内存中开辟了⼀个相应的⽂件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名 FILE

 例如,VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

struct _iobuf {char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsiz;char *_tmpfname;};typedef struct _iobuf FILE;

这以上就可以看出FILE是对结构体struct _iobuf的重命名

注意以上只是VS2013下的文件类型申明,不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异

每当打开⼀个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信

息,这在当中是怎么将信息填入和填入的有哪些信息我们不必关注

所以这时就将文件信息区的地址放在指针变量里,所以就可以通过⼀个FILE的指针来维护这个FILE结构的变量,例如就可以用FILE* p

在以上提到的标准流stdin、stdout、stderr 三个流的类型也是: FILE *

例如可以创建⼀个FILE *pf的指针变量:可以使pf指向某个文件文件信息区(是⼀个结构体变

量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。

例如:

4.3 文件的打开和关闭 

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建立了指针和文件的关系。

ANSI C 规定使用 fopen 函数来打开文件fclose 来关闭文件

 在fopen中打开文件的方式:

⽂件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开⼀个已经存在的⽂本⽂件 出错
“w”(只写) 为了输出数据,打开⼀个文本⽂件 建⽴⼀个新的⽂件
“a”(追加) 向⽂本⽂件尾添加数据 建⽴⼀个新的⽂件
“rb”(只读) 为了输⼊数据,打开⼀个⼆进制⽂件 出错
“wb”(只写) 为了输出数据,打开⼀个⼆进制⽂件 建⽴⼀个新的⽂件
“ab”(追加) 向⼀个二进制⽂件尾添加数据 建⽴⼀个新的⽂件
“r+”(读写) 为了读和写,打开⼀个⽂本⽂件 出错
“w+”(读写) 为了读和写,建议⼀个新的⽂件 建⽴⼀个新的⽂件
“a+”(读写) 打开⼀个⽂件,在⽂件尾进⾏读写 建⽴⼀个新的⽂件
“rb+”(读写) 为了读和写打开⼀个⼆进制⽂件 出错
“wb+”(读

写)

为了读和写,新建⼀个新的⼆进制⽂件 建⽴⼀个新的⽂件
“ab+”(读

打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写 建⽴⼀个新的⽂件

例如以下打开与关闭文件代码

#include <stdio.h>int main(){//打开文件test.txtFILE* pf=fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}//进行操作////关闭文件fclose(pf);pf = NULL;return 0;}

注意:在打开文件时,使用fopen时要判断返回值是否是NULL,如果是就不能继续进行操作,需要在此跳出程序。同时在使用fclose关闭文件后要将文件指针置为空指针,从而避免该指针变为野指针

5. 文件的顺序读写 

5.1 顺序读写函数介绍

1.fputc和fgetc

fputc

fputc的作用是将字符输出到文件当中,当然在这当中也是将字符先输入到流里,之后的工作由流来实现,在putc函数中的参数有两个第一个是所要输出的字符,第二个是要输出对象的文件指针。该函数输出成功后,返回值为所写字符,如果发生写入错误,返回值就为EOF

注:在使用fputc时也是要先打开文件,同时必须以的方式打开,不能以读的方式打开文件,否则程序会发生错误 

以下是该函数的使用举例

#include<stdio.h>int main(){//打开文件test.txtFILE* pf=fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}//进行操作for (int i = 'a'; i<= 'z'; i++){fputc(i, pf);}//关闭文件fclose(pf);pf = NULL;return 0;}

fgetc 

fgetc的作用是将当前指定的内部文件指向的字符返回,同时内部文件位置指示器将前进到下一个字符。该函数的参数就是要输入对象的文件指针

使用fgetc读取文件成功后,就返回读取的字符,读取失败或者读取到文件的末尾就返回EOF

注:在使用fgetc时也是要先打开文件,同时必须以的方式打开,不能以写的方式打开文件,否则程序会发生错误 

以下是该函数的使用举例

#include<stdio.h>int main(){//打开文件test.txtFILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}//进行操作int ch = 0;while ((ch=fgetc(pf)) != EOF){printf("%c", ch);}//关闭文件fclose(pf);pf = NULL;return 0;}

2.fputs和fgets

fputs

fputs的作用是将字符串写入文件指针相关联的文件流中,在puts函数中的参数有两个第一个是所要输出的字符串,第二个是要输出对象的文件指针。

当在使用fputs时输出成功后返回值为非负值,输出失败后返回EOF,并设置错误标识

注:在使用fputs时也是要先打开文件,同时必须以的方式打开,不能以读的方式打开文件,否则程序会发生错误 

在使用该函数时,若在输出时未换行将下一次输出字符串将在上一次的末尾开始输入

以下是该函数的使用举例

int main(){//打开文件test.txtFILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}//进行操作fputs("abcdef", pf); fputs("hello", pf);//关闭文件fclose(pf);pf = NULL;return 0;}

 fgets

 fgets的作用是从流中获取字符串输入到字符数组中,直到读取 (num-1) 个字符或到达换行符或文件末尾,在pgets函数中的参数有三个第一个是所要输入字符串的数组指针,第二个是输入过程中最大拷贝字符个数,第三个是要输入对象的文件指针。

若使用fgets成功后返回值为指针str,若失败返回空指针NULL,同时设置错误标识

注:在使用fgets时也是要先打开文件,同时必须以读的方式打开,不能以写的方式打开文件,否则程序会发生错误

以下是该函数的使用举例

include<stdio.h>int main(){FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}char arr[20] = { 0 };fgets(arr, 10, pf);printf("%s", arr);fclose(pf);pf = NULL;return 0;}

当在test.txt中写入以下字符时,调试以上代码观察fgets的输入

在以上调试就可以看出当文件当中的第一行只有6个字符时最多就只能拷贝6个字符,不会再拷贝下一行的数据

 

3.fwrite和fread

fwrite

fwrte的作用是将数组ptr以二进制的形式输出到流中,数组中每个元素大小为size,元素个数为count ,fwrite中有三个参数,第一个是数组的指针,第二个是数组中每个元素的大小,第三个是数组元素的个数,最后一个是输出的文件指针

该函数的返回值是成功写入到文件当中的元素个数,但如果写入个数与参数数组个数count不相等则该函数就无法实现,且这时程序会设置错误标识

注:在使用fwrite时也是要先打开文件,同时必须以的方式打开,不能以读的方式打开文件,否则程序会发生错误 

在使用fwrite时打开文件的方式与之前用到的函数不同写文件用到的时wb,例如以下代码就是将数据以二进制的形式写入文件当中

以下是该函数的使用举例

struct S{char name[10];int age;double sorce}s={"Lisi",18,82.5};int main(){FILE* pf = fopen("test.txt", "wb");if (pf == NULL){perror("fopen");return 1;}fwrite(&s, sizeof(struct S), 1, pf);fclose(pf);pf = NULL;return 0;}

这时在以上代码运行后打开文件就会发现文件中存放的是二进制数据,直接打开文件就会出现乱码

这时就需要了解在VS中打开二进制的文件 

这时以二进制方式打开data.txt就可以看到文件中存放的数据如下

 fread

fread的作用是将流中二进制的数据读取到数组ptr中,数组中每个元素大小为size,元素个数为count ,fread中有三个参数,第一个是数组的指针,第二个是数组中每个元素的大小,第三个是数组元素的个数,最后一个是输出的文件指针

该函数的返回值是成功读取到文件当中的元素个数,但如果读取个数与参数数组个数count不相等则该函数就无法实现,且这时程序会设置错误标识

注:在使用fwrite时也是要先打开文件,同时必须以读的方式打开,不能以写的方式打开文件,否则程序会发生错误 

在使用fread时打开文件的方式与之前用到的函数不同读文件用到的时rb,例如以下代码就是文件中读取到s中并将结果打印到屏幕上

include<stdio.h>struct S{char name[10];int age;double sorce;}s = { 0 };int main(){FILE* pf = fopen("test.txt", "rb");if (pf == NULL){perror("fopen");return 1;}fread(&s, sizeof(struct S), 1, pf);printf("%s %d %lf", s.name, s.age, s.sorce);fclose(pf);pf = NULL;return 0;}

4.scanf/fscanf/sscanf/printf/fprintf/sprintf

在之前的学习中我们已经了解了scanf与printf的使用方法,printf只能将数据输出到屏幕当中,scanf只能读取键盘上输入的数据,使用如果要针对所有输入,输出流这两个函数就不能实现功能了,这时就要用到fscanf与fprintf‘这两个函数

1.fscanf和fprintf

fcanf

fscanf的作用是从流中读取格式化的数据,该函数相比scanf参数只多了一个文件指针,但fscanf能实现所有流输入

该函数的返回值是读取格式化数据的个数,如果在读取时发生读取错误或到达文件末尾则返回EOF,且这时程序会设置错误标识

以下是该函数的使用举例

include<stdio.h>struct S{char name[10];int age;double sorce;}s = {0};int main(){FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}fscanf(pf, "%s %d %lf", s.name, &(s.age), &(s.sorce));printf("%s %d %lf", s.name, s.age, s.sorce);fclose(pf);pf = NULL;return 0;}

如果先创建test.txt这个文档并且输入以下数据

以上代码输出结果如下所示

 fprintf

fprintf的作用是将格式化的数据输出到流中,该函数相比printf参数只多了一个文件指针,但fprintf能实现所有流输出

该函数的返回值是输入格式化数据的个数,如果在输入时发生读取错误或到达文件末尾则返回EOF,且这时程序会设置错误标识

以下是该函数的使用举例

include<stdio.h>struct S{char name[10];int age;double sorce;}s={"Lisi",18,82.66};int main(){FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}fprintf(pf,"%s %d %lf", s.name, s.age,s.sorce);fclose(pf);pf = NULL;return 0;}

以上代码运行完后test.txt文件内写入数据如下 

 

2.ssanf和sprint

sscanf

sscanf的作用是将是将字符串中的数据读取到格式化数据当中

该函数的返回值是字符串输入到格式化数据的个数,如果在读取时发生读取错误或到达文件末尾则返回EOF,且这时程序会设置错误标识

以下是该函数的使用举例

include<stdio.h>struct S{char name[20];int age;double sorce;}s = { 0 };int main(){char* p = "Zhangshang 20 90.3 ";sscanf(p, "%s %d %lf", s.name, &(s.age), &(s.sorce));fprintf(stdout, "%s %d %lf", s.name, s.age, s.sorce);return 0;}

输出结果如下 

sprintf

sprintf的作用是将是将格式化数据转换为字符串

该函数的返回值是格式化数据输出到字符串中的个数,如果在读取时发生读取错误或到达文件末尾则返回EOF,且这时程序会设置错误标识

以下是该函数的使用举例

include<stdio.h>struct S{char name[20];int age;double sorce;}s = { "Zhangshang",20,90.3 };int main(){char arr[100] = {0};sprintf(arr, "%s %d %lf", s.name, s.age, s.sorce);fprintf(stdout, "%s", arr);return 0;}

输出结果如下 

 函数总结

函数名 功能 适用于
fgetc 字符输入函数 所有输入流
fputc 字符输出函数 所有输出流
fgets 文本行输入函数 所有输入流
fputs 文本行输出函数 所有输出流
fread

二进制输入

文件输入流
fwrite 二进制输出 文件输出流

fscanf

格式化输入函数 所有输入流
fprintf 格式化输出函数 所有输出流
sscanf 格式化输入函数 所有输入流
sprintf 格式化输出函数 所有输出流

6. 文件的随机读写

6.1 fseek

fseek的作用是用来重新指定文件指针,根据⽂件指针的位置和偏移量来定位⽂件指针(文件内容的光标)该函数的参数有三个,第一个是文件指针,第二个是目标位置相较起始位置的偏移量,第三是起始位置指针

该函数如果成功,该函数将返回零。否则,它将返回非零值。

其中起始位置有以下三种情况 

以下是该函数的使用举例

include<stdio.h>int main(){FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}fputs("hello", pf);fseek(pf, -4, SEEK_END);fputs("xxx", pf);fclose(pf);pf = NULL;return 0;}

以上代码先将hello输入到test.txt文件当中,再使用fseek使得光标从o后跳到了h后,再在文件中输入xxx这时2文件的ell就会被xxx替代,文件内容就变为hxxxo

6.2 ftell

ftell的作用是返回文件指针相对于起始位置的偏移量

该函数的参数是文件指针

ftell的返回值是返回位置指标的当前值

以下是该函数的使用举例

include<stdio.h>int main(){FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}int ch = 0;ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);printf("%c\n", fgetc(pf));printf("%d", ftell(pf));fclose(pf);pf = NULL;return 0;}

 先在test.txt文件中写入hello

输出结果如下

6.3 rewind

rewind的作用是让文件指针的位置回到文件的起始位置

该函数的参数是文件指针

该函数无返回值

以下是该函数的使用举例

include<stdio.h>int main(){FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}int ch = 0;ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);rewind(pf);printf("%d", ftell(pf));fclose(pf);pf = NULL;return 0;}

 输出结果如下

7. 文件读取结束的判定 

牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束

feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。

1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )

例如:

• fgetc 判断是否为 EOF .

• fgets 判断返回值是否为 NULL .

2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

例如:

• fread判断返回值是否小于实际要读的个数。

文本文件的例子:

#include <stdio.h>#include <stdlib.h>int main(void){int c; // 注意:int,⾮char,要求处理EOFFILE* fp = fopen("test.txt", "r");if(!fp) {perror("File opening failed");return EXIT_FAILURE;}//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOFwhile ((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);}

二进制文件的例子: 

#include <stdio.h>enum { SIZE = 5 };int main(void){double a[SIZE] = {1.,2.,3.,4.,5.};FILE *fp = fopen("test.bin", "wb");fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组fclose(fp);double b[SIZE];fp = fopen("test.bin","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("%f ", 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. 文件缓冲区 

ANSIC 标准采⽤“缓冲文件系统” 处理的数据文件的,所谓缓冲文件系统是指系统⾃动地在内存中为程序中每⼀个正在使用的⽂件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。