C语言文件操作【基础知识 + 顺序读写 + 文件版通讯录】
全文目录
- 😀 前言
- 🤔 什么是文件
- 😶 程序文件
- 😶 数据文件
- 😶 文件名
- 🤨 文件指针
- 🤫 文件的打开和关闭
- 😑 `fopen` 打开文件
- 📙 **mod的规律:**
- 😑 `fclose` 关闭文件
- 📙 文件打开不关闭的后果
- 😵💫 文件的顺序读写
- 🙄 **`fputc` 字符输出函数**
- 🙄 **`fgetc` 字符输入函数**
- 🙄 `fputs` 文本行输出函数
- 🙄 **`fgets` 文本行输入函数**
- 🙄 `fprintf` 格式化输出函数
- 🙄 `fscanf` 格式化输入函数
- 🙄 `fwrite` 二进制输出函数
- 🙄 `fread` 二进制输入函数
- 😏 三组 `scanf` 和 `printf` 函数对比
- 😍 文件版通讯录
- 🙂 将通讯录信息保存在文件中
- 🙂 加载文件中的通讯录信息
- 🌈 总结
😀 前言
前面写的程序都是一闪即逝,只要关闭了程序,就找不到运行的结果。如果想要将运行的结果保存下来,下次运行的时候接着上次运行,只有自己想要删除数据,数据才会消失。这就涉及到了数据持久化的问题, 我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
现在就学习一下使用文件,我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
🤔 什么是文件
文件是计算机文件属于文件的一种,与普通文件载体不同,计算机文件是以计算机硬盘为载体存储在计算机上的信息集合。文件可以是文本文档、图片、程序等等。文件通常具有三个字母的文件扩展名,用于指示文件类型(例如,图片文件常常以 JPEG 格式保存并且文件扩展名为 .jpg)。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
😶 程序文件
程序文件存储的是程序,包括源程序和可执行程序。
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
😶 数据文件
数据文件比较多,数据的概念也比较广泛图形图像声音数字各种码制都是数据,存储这些数据的文件就是数据文件。
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本文就是来学习一下数据文件。
以前我们写的代码所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
但是有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
😶 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt
为了方便起见,文件标识常被称为文件名
🤨 文件指针
文件类型指针简称文件指针。
所谓的文件类型其实就是一个结构体类型:
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
不同的C编译器的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结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE
的指针来维护这个FILE
结构的变量,这样使用起来更加方便。结构体变量毕竟太大了。
// demo
FILE* pf; // 文件指针变量
定义pf
是一个指向FILE
类型数据的指针变量。可以使pf
指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
🤫 文件的打开和关闭
使用文件之前需要打开文件,使用完要关闭文件(这个很重要,后面说)。
ANSIC
规定使用fopen
函数来打开文件,fclose
来关闭文件。
😑 fopen
打开文件
函数描述:
filename
:文件名
文件名(路径 + 文件名)。如果没有指定路径,默认是在当前源文件的路径下(相对路径)。
注意: 路径的\
字符需要转义:\\
mod
:打开文件的方式
📙 mod的规律:
文件不存在时:
如果是
w
都是创建新的文件,r
都是出错,a
除了ab
都是创建新文件文件存在时:
如果文件存在并且只是以
w
的形式打开文件(包括wb
)。文件的内容会被清空双重形态
读带上
+
就是读写, 追加和写带上+
就是读写,否则都只能读或者写。
返回值 Return Value
:
如果文件被成功打开,函数将返回一个指向
file
对象的指针。否则,将返回一个空指针。
在大多数库实现中,
errno
变量也设置为失败时的系统特定错误代码。
😑 fclose
关闭文件
函数描述:
关闭文件流stream
。
返回值Return Value
:
如果成功了返回0。
失败返回
EOF
。
// demo
fclose(pf);
pf = NULL;
📙 文件打开不关闭的后果
1. 如果不关闭文件,可能会导致文件临时数据丢失,造成文件写入失败。因为文件数据在关闭之前不保存在磁盘,而是保存在运行内存中,关闭或者主动刷新才写入磁盘。
2. 还有文件没有关闭,会浪费系统的文件描述符的分配。
😵💫 文件的顺序读写
输出就是程序向文件里面写数据, 输入就是程序从文件里面读数据。读(输入)写(输出)都是以程序为参照物。
顺序读写就是以文件从前往后依次读或写。
顺序读写函数:
适应所有输入流的函数,只需要将文件指针改为标准输入输出就可以实现标准的输入输出了。
🙄 fputc
字符输出函数
函数描述:
将字符character
写入steam
指向的文件中。
返回值Return Value
:
写入成功返回被写入的字符。
写入失败返回
EOF
,并且设置错误码 。
// demo
for (char ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
🙄 fgetc
字符输入函数
函数描述:
从steam
指向的文件中读取一个字符。
返回值 Return Value
:
如果成功读取返回读取字符的ASCII码值。
如果读取到文件的末尾,返回
EOF
,并且为steam
设置eof (end of file)
。如果发生阅读错误同样返回
EOF
,但是会改为设置错误码。
// demo
char ch;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
🙄 fputs
文本行输出函数
函数描述:
将字符串 str
输出到 steam
指向的文件中。
返回值 Return Value
:
成功时,将返回非负值。
出错时,该函数返回 EOF 并设置错误指示器(ferror)。
// demo
fputs("qwertyuiop", pf); // 注意该函数不会主动添加\n
🙄 fgets
文本行输入函数
函数描述:
从steam
指向的文件中读取字符并将其作为 C 字符串存储到 str
中,直到读取 (num-1)
个字符或到达换行符或文件末尾,以先发生者为准。
返回值Return Value
:
成功后,函数返回
str
。如果在尝试读取字符时遇到文件末尾,则设置
eof
指示器 (feof
)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(str
的内容保持不变)。如果发生读取错误,则设置错误指示器(
ferror
),并返回空指针(但str
指向的内容可能已更改)。
// demo
char str[256];
fgets(str, 256, pf);
printf("%s", str);
🙄 fprintf
格式化输出函数
函数描述:
将按格式指向流的 C 字符串写入流。如果 format
包含格式说明符(以 %
开头的子序列),则格式后面的其他参数将被格式化并插入到生成的字符串中,替换其各自的说明符。
在 format
参数之后,函数至少需要与格式指定的一样多的其他参数。
返回值Return Value
:
成功后,将返回写入的字符总数。
如果发生写入错误,则设置错误指示器(
ferror
)并返回负数。如果在写入宽字符时发生多字节字符编码错误,
errno
将设置为EILSEQ
并返回负数。
文档中对于fprintf
的介绍巴拉巴拉一大堆,看着就头大。我们可以对比一下printf
可以看到 fprintf
对于 printf
只多了一个文件指针,所以使用fprintf
只需要比printf
多传一个文件指针就行了。
// demo
struct Student
{
char name[10];
int age;
char phone[12];
} s = { "张三", 19, "1008611" };
fprintf(pf, "%s %d %s", s.name, s.age, s.phone);
🙄 fscanf
格式化输入函数
函数描述:
从流中读取数据,并根据参数格式将其存储到附加参数所指向的位置。
附加参数应指向已分配的对象,该对象的类型由格式字符串中相应的格式说明符指定。
返回值Return Value
:
成功后,函数将返回成功填充的参数列表中的项数。此计数可能与预期的项数匹配,也可能由于匹配失败、读取错误或到达文件末尾而减少(甚至为零)。
如果在读取时发生读取错误或到达文件末尾,则会设置正确的指示器(feof或
ferror
)。并且,如果在成功读取任何数据之前发生任何一种情况,则返回
EOF
。
如果在解释宽字符时发生编码错误,函数会将errno
设置为EILSEQ
。
同样的对比scanf
结果一目了然,只需要在使用scanf
的基础上加上一个文件指针就行了。
// demo
struct Student
{
char name[10];
int age;
char phone[12];
} s;
fscanf(pf, "%s%d%s", &s.name, &s.age, &s.phone);
printf("%s %d %s\n", s.name, s.age, s.phone);
🙄 fwrite
二进制输出函数
函数描述:
从ptr指向的内存块向流stream
中的当前位置写入count
个元素的数组,每个元素的大小为size
字节。
流的位置指示符提前写入的字节总数。
在内部,该函数将ptr
指向的块解释为无符号char
类型的(size*count
)元素数组,并将它们按顺序写入流,就像为每个字节调用fputc
一样。
返回值 Return Value
:
返回成功写入的元素总数。
如果此数字与计数参数不同,则写入错误会阻止函数完成。在这种情况下,将为流设置错误指示器(
ferror
)。如果大小或计数为零,则函数返回零,并且错误指示器保持不变。
// demo
struct Student
{
char name[10];
int age;
char phone[12];
} s = { "张三", 19, "1008611" };
fwrite(&s, sizeof(s), 1, pf);
🙄 fread
二进制输入函数
函数描述:
从流中读取count
个元素的数组,每个元素的大小为size
字节,并将它们存储在ptr
指定的内存块中。
流的位置指示符提前读取的字节总数。
如果成功,读取的字节总数为(size*conut
)。
返回值Return Value
:
返回成功读取的元素总数。
如果此数字与
count
参数不同,则表示发生读取错误,或者读取时已到达文件末尾。在这两种情况下,都设置了适当的指示器,可以分别用ferror
和feof
进行检查。如果大小或计数为零,则函数返回零,并且ptr所指向的流状态和内容保持不变。
// demo
struct Student
{
char name[10];
int age;
char phone[12];
} s;
fread(&s, sizeof(s),1, pf);
printf("%s %d %s\n", s.name, s.age, s.phone);
😏 三组 scanf
和 printf
函数对比
先来了解一下 sprintf
和 sscanf
:
与格式化输出、输入函数百分之九十都是相似的,使用方法就不言而喻了。
然后总体来看一下这三组函数:
三组函数的参数都是基本相似的,只是使用的对象不同。
😍 文件版通讯录
有了上面的文件操作知识,我们就可以将之前写动态内存版本的通讯录再进一步的改善,只需要改动两个点:
- 打开通讯录时,需要加载磁盘上的通讯录信息
- 关闭通讯录时,需要将通讯录信息保存在磁盘上
这里使用的二进制的读写,因为是在想不出文本读写怎么操作(尴尬)。
🙂 将通讯录信息保存在文件中
// 将通讯录内容刷新到磁盘上
void SaveContact(const Contact* con)
{
assert(con);
// 文件打开
FILE* pf = fopen("contact.dat", "wb");
if (pf == NULL)
{
perror("SaveContact::fopen");
return;
}
// 输出
for (int i = 0; i < con->size; i++)
{
fwrite(con->data + i, sizeof(PeoInform), 1, pf);
}
// 关闭文件
fclose(pf);
pf = NULL;
}
🙂 加载文件中的通讯录信息
// 加载磁盘上的通讯录内容
void LoadContact(Contact* con)
{
assert(con);
// 打开文件
FILE* pf = fopen("contact.dat", "rb");
if (pf == NULL)
{
perror("LoadContact::fopen");
return;
}
// 输入
int i = 0;
while (fread(con->data + i, sizeof(PeoInform), 1, pf))
{
CheckCapacity(con);
i++, con->size++;
}
// 文件关闭
fclose(pf);
pf = NULL;
}
源码: 文件版通讯录
🌈 总结
文件操作在日后的编程中将会显得愈发重要,这些知识文件的冰山一角,所以一定要好好掌握这些知识点。
下一章: 文件的随机读写