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

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 头文件中有以下的文件类型申明:
cpp
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+"(读 写 | 打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写 | 建⽴⼀个新的⽂件 |
例如以下打开与关闭文件代码
cpp
#include <stdio.h>
int main()
{
//打开文件test.txt
FILE* 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时也是要先打开文件,同时必须以写的方式打开,不能以读的方式打开文件,否则程序会发生错误
以下是该函数的使用举例
cpp#include<stdio.h> int main() { //打开文件test.txt FILE* 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时也是要先打开文件,同时必须以读****的方式打开,不能以写的方式打开文件,否则程序会发生错误
以下是该函数的使用举例
cpp#include<stdio.h> int main() { //打开文件test.txt FILE* 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时也是要先打开文件,同时必须以写的方式打开,不能以读的方式打开文件,否则程序会发生错误
在使用该函数时,若在输出时未换行将下一次输出字符串将在上一次的末尾开始输入
以下是该函数的使用举例
cppint main() { //打开文件test.txt FILE* 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时也是要先打开文件,同时必须以读 的方式打开,不能以写的方式打开文件,否则程序会发生错误
以下是该函数的使用举例
cpp
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,例如以下代码就是将数据以二进制的形式写入文件当中
以下是该函数的使用举例
cppstruct 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中并将结果打印到屏幕上
cppinclude<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, 且这时程序 会设置错误标识
以下是该函数的使用举例
cppinclude<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, 且这时程序 会设置错误标识
以下是该函数的使用举例
cppinclude<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和sprintf
sscanf

sscanf的作用是将是将字符串中的数据读取到格式化数据当中
该函数的返回值是字符串输入到格式化数据的个数,如果在读取时发生读取错误或到达文件末尾则返回EOF, 且这时程序 会设置错误标识
以下是该函数的使用举例
cppinclude<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, 且这时程序 会设置错误标识
以下是该函数的使用举例
cppinclude<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的作用是用来重新指定文件指针,根据⽂件指针的位置和偏移量来定位⽂件指针(文件内容的光标)该函数的参数有三个,第一个是文件指针,第二个是目标位置相较起始位置的偏移量,第三是起始位置指针
该函数如果成功,该函数将返回零。否则,它将返回非零值。
其中起始位置有以下三种情况
以下是该函数的使用举例
cpp
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的返回值是返回位置指标的当前值
以下是该函数的使用举例
cppinclude<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的作用是让文件指针的位置回到文件的起始位置
该函数的参数是文件指针
该函数无返回值
以下是该函数的使用举例
cppinclude<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; }
输出结果如下