本节内容详尽描述了C语言文件读写的内容,让你再也不担心IO持久化的问题啦!
目录
[1. 文件指针](#1. 文件指针)
[2. 文件的打开与关闭](#2. 文件的打开与关闭)
[3. 文件的顺序读写](#3. 文件的顺序读写)
[3.1 fputc的使用](#3.1 fputc的使用)
[3.2 fgetc函数的使用](#3.2 fgetc函数的使用)
[3.3 fputs函数](#3.3 fputs函数)
[3.4 fgets函数](#3.4 fgets函数)
[3.5 fprintf 函数](#3.5 fprintf 函数)
[3.6 fscanf函数](#3.6 fscanf函数)
[3.7 什么是"流"](#3.7 什么是“流”)
[3.8 二进制的输入和输出](#3.8 二进制的输入和输出)
[4. 函数对比](#4. 函数对比)
[4.1.1 sprintf函数](#4.1.1 sprintf函数)
[4.2 scanf/fscanf/sscanf](#4.2 scanf/fscanf/sscanf)
[4.2.1 sscanf函数](#4.2.1 sscanf函数)
[5. 改造通讯录](#5. 改造通讯录)
[5.1 通讯录(动态内存 + 可持久化版)完整代码](#5.1 通讯录(动态内存 + 可持久化版)完整代码)
[6. 文件的随机读写](#6. 文件的随机读写)
[6.1 fseek函数](#6.1 fseek函数)
[6.2 ftell](#6.2 ftell)
[6.3 rewind函数](#6.3 rewind函数)
[7. 文本文件和二进制文件](#7. 文本文件和二进制文件)
[7.1 一个数据在内存中是怎么存储的呢?](#7.1 一个数据在内存中是怎么存储的呢?)
[8. 文件读取结束的判定](#8. 文件读取结束的判定)
[8.1 被错误使用的feof](#8.1 被错误使用的feof)
1. 文件指针
缓冲文件系统中,关健的概念是"文件类型的指针",简称文件指针。每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用于存放文件的相关信息(文件的名字、状态、当前文件的位置)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为FILE。
例如在VS2013编译环境提供的stdio.h头文件中有以下文件类型声明:
cpp
struct _iobuf{
char* _ptr;
int _cnt;
char* _base;
int _flag;
......
};
typedef struct _iobuf FILE;
在打开一个文件的时候,在内存中会生成一个文件信息区,使用fopen函数会得到这个文件信息区的起始地址,这个地址的类型是FILE*也就是文件指针。也就是说这个文件信息区和被打开的文件已经形成了关联。

下面我们可以创建一个FILE*类型的变量,这个指针可以指向某个文件的文件信息区。
cpp
FILE* pf; // 文件指针变量
2. 文件的打开与关闭
文件读写的流程可以分为以下三步:
1)文件打开;
2)文件操作;
3)文件关闭;
接下来我们学习文件的打开与关闭,使用fopen、fclose函数进行文件的打开与关闭,以下是这两个函数的具体用法:
cpp
// 打开文件
FILE* fopen(const char* filename,const char* mode);
// 关闭文件
int fclose(FILE* stream);
首先看文件打开函数fopen,返回的是一个FILE类型的指针,第一个形参是文件名,这里需要注意,这里的文件名其实指的是:路径+文件名+文件后缀,这也很好理解,文件名必须是独一无二的;再看看第二个参数mode,其实就是打开的方式,打开文件是为了读取、写入?这需要做出规定,打开文件的模式有以下几种:r代表只读、w代表只写、a代表追加、r+代表能读也能修改、w+代表能写也能修改、a+代表能追加也能修改。

我们举一个例子辅以图片来更好地了解fopen的原理:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
FILE* pf = fopen("text.txt","r");
return 0;
}
首先使用fopen为文件开辟了一块内存信息区,返回给了pf指针,接下来我们只需要通过pf指针来对文件信息区进行修改,最终会影响到文件本身。

这里文件打开有可能失败,所以上面这么写是非常不安全的,所以我们可以做以下改进:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("text.txt","r");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// 非空指针才能进行操作
return 0;
}
文件的关闭非常简单,只需要给fclose传入文件指针即可,注意这里的文件关闭之后需要将pf置为空,否则pf会变成野指针,这里很好理解:fclose就类似于free将内存中的空间收回(这里不懂1可以去看笔者写的动态内存管理博文),那么指针不能保留已经被收回的空间的地址,所以要置为空。
cpp
// 文件的关闭
fclose(pf);
pf = NULL;
此时运行这段代码会报错:没有这个文件或者文件夹,也很好理解,当前路径下确实没有这个文件存在,只需要创建好这个文件即可。

我们也可以使用绝对路径,打开指定文件,这里需要注意使用双\进行转义,当读取到文件的时候便不会再报错。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt","r");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// 非空指针才能进行操作
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}
3. 文件的顺序读写
文件的打开就是为了文件的读写,我们接下来介绍文件的顺序读写;常见的函数如下图所示:

3.1 fputc的使用
cpp
int fputc(int char,FILE*stream);
有两个参数,第一个是字符,第二个是FILE*的指针,其实就是将一个字符写入到某个文件中,这里的字符为什么是int类型呢?这是因为只需要字符的ascii码值就行。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt","w");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// fputc
fputc('a',pf );
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}
程序运行结束之后,我们可以查看相应的文件内容:

3.2 fgetc函数的使用
cpp
int fgetc(FILE* stream);
传入一个FILE*指针就能读取到一个字符。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt","r");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// fgetc
int c = fgetc(pf);
printf("%c\n",c);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}

当读取失败的时候,函数会返回一个EOF,所以如果我们想让读取文件所有的内容,我们可以这样做:1.现在文件写一堆字符串。2.利用循环读取所有字符遇到EOF为止。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt","r");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// fgetc
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}

如此以来我们就能读取字符串了。
3.3 fputs函数
cpp
int fputs(const char* str,FILE* stream);
函数的用法基本一致,第一个参数相交于fgetc可以写一整个字符串,第二个参数是FILE*指针。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt","w");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// fputs
fputs("hello new world!",pf);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}

执行完毕后检查文件,发现之前写的内容被覆盖掉了,要想内容不背覆盖,我们可以在打开文件的时候使用"a"追加模式(append)。
3.4 fgets函数
cpp
char* fgets(char* str,int num,FILE* stream);
第一个参数读取之后的字符串存放的位置,第二个参数是读取的字节数,第三个参数是文件指针。
返回值就是读取成功的字符串的地址,读取失败返回空指针。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt", "r");
if (pf == NULL)
{
printf(strerror(errno));
return 1;
}
// fgets
char str[20];
fgets(str, 5, pf);
printf("%s\n",str);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}
这里读5个字符试一下,我们发现这里只是读取了4个字符:这是因为当读取5个字符的时候,其实是最大为5个字符,为了存放\0,所以只能读取4个字符。

讲到这里,上面的打印错误信息的方法其实可以再简便一些:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.tx", "r");
if (pf == NULL)
{
/*printf(strerror(errno));*/
perror("fopen");
return 1;
}
// fgets
char str[20];
fgets(str, 5, pf);
printf("%s\n",str);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}
这样一来,我们看一下输出的结果:在perror中给一个字符串,这样就会在输出的时候带上这个字符串再加一个:,表示错误的来源,完全可以替代strerror(errno)的写法。

3.5 fprintf 函数
当我们要以格式化的方式写入文件,可以使用这个函数,我们可以写一个结构体变量到文件中去。
cpp
int fprintf(FILE* stream,const char* format,...);
如果看不懂参数,我们可以和printf进行对标学习:只是前面多了一个FILE*的结构体

cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = {"Lydia",18,90.5f};
FILE* pf = fopen("E:\\DeskTop\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// fgets
char str[20];
fprintf(pf,"my name is %s, I am %d years old, my math score is %.2f",s.name,s.age,s.score);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}
按照某种格式写入文件,非常地简单。

3.6 fscanf函数
cpp
int fscanf(FILE* stream,const char* format);
用法类似,这里不再赘述。

cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("E:\\DeskTop\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// fgets
char str[20];
fscanf(pf, "my name is %5s, I am %2d years old, my math score is %f", &s.name, &(s.age), &(s.score));
printf("name:%s\n", s.name);
printf("age:%d\n", s.age);
printf("score:%.2f\n", s.score);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}
需要注意的是,在读取的时候和scanf一样只需要得到变量的地址即可,如果是从字符串读取指定内容需要添加限定位宽,避免缓冲区溢出。

3.7 什么是"流"
当我们调用上面的函数,就会返回一个FILE*指针,这个指针我们可以称作为文件流,其实程序员只需要对"流"进行操作,就可以间接地对文件进行操作,所以C语言就封装了一个"流"帮助我们间接地控制文件。
一个C语言的程序的运行需要三个流,stdin标准输入流(键盘)、stdout标准输出流(屏幕)、stderr标准错误流(屏幕 ),它们的类型都是FILE*。
那么既然fprintf是一个输出流,那么我们可不可以将fprintf输出到屏幕上?答案是可以的!只需要将指针换成stdout即可。
cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = { "Lele",19,89.7 };
FILE* pf = fopen("E:\\DeskTop\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// fprintf
char str[20];
fprintf(stdout, "my name is %5s, I am %2d years old, my math score is %.2f", s.name, s.age, s.score);
// 文件的关闭
fclose(pf);
pf = NULL;
return 0;
}

3.8 二进制的输入和输出

第一个参数,写入文件的数据源的指针; 第二个参数,元素的大小;第三个参数;第四个参数,元素的个数,文件指针。
cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = { "Lele",19,89.7 };
FILE* pf = fopen("E:\\DeskTop\\test.txt", "wb"); // b代表binary二进制
// 将结构体变量以二进制形式写入文件
if (pf == NULL)
{
perror("fopen");
}
fwrite(&s, sizeof(struct S), 1, pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
写入文件,我们压根看不懂,这是以二进制的方式写入的。

为了能够读取二进制数据,我们需要一个读取二进制文件的函数:

观察函数形参基本上一致。
cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("E:\\DeskTop\\test.txt", "rb"); // b代表binary二进制
// 将结构体变量以二进制形式写入文件
if (pf == NULL)
{
perror("fopen");
}
fread(&s, sizeof(struct S), 1, pf);
printf("name:%s\n", s.name);
printf("age:%d\n", s.age);
printf("score:%.2f\n", s.score);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}

4. 函数对比
4.1printf/fprintf/sprintf
printf是针对标准输出的格式化输入语句。
fprintf是针对所有输出流的格式化输入语句。
sprintf把一个格式化的数据写入到字符串。
4.1.1 sprintf函数

把一个格式化的数据写入到字符串中。
cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = { "yusanshi",20,88.1};
char* buf[100];
sprintf(buf,"%s %d %.2f",s.name,s.age,s.score);
printf("%s\n",buf);
return 0;
}

4.2 scanf/fscanf/sscanf
scanf是针对标准输入的格式化输入语句。
fscanf是针对所有输入流的格式化输入语句。
sscanf从一个字符串中提取结构化数据。
4.2.1 sscanf函数
从一个字符串中提取结构化数据,将存入buf中的数据存入结构体变量tmp中。

cpp
#include<stdio.h>
struct S
{
char name[10];
int age;
float score;
};
int main()
{
struct S s = { "yusanshi",20,88.1};
struct S tmp = { 0 };
char* buf[100];
sprintf(buf,"%s %d %.2f",s.name,s.age,s.score);
sscanf(buf,"%s %d %f", tmp.name,&(tmp.age), &(tmp.score));
printf("%s\n", tmp.name);
printf("%d\n", tmp.age);
printf("%.2f\n", tmp.score);
return 0;
}
我们需要注意的是,在读取结构体数据写入字符串的时候需要给空格以示区分,不然的话在读取字符串的时候就无法区分了。
这两个函数不涉及流的概念,应用场景可以是BS架构的项目,这两个函数在公司的面试中经常提到。
5. 改造通讯录
博主之前的结构体小练习:通讯录项目在动态内存管理的时候已经改造过了一次,上次改造将定长数组改成了变长数组(堆开辟空间)。这次我们学习了文件的读写,我们可以将数据进行持久化存储了。
需求:
在退出通讯录的时候将数据持久化到本地,下次执行的时候能够读取到通讯录信息。
修改1:在主函数的退出部分增加一个保存函数。

修改2:头文件声明、逻辑文件实现保存数据。
cpp
// 通讯录数据持久化
void saveContact(const Contact* p)
{
assert(p);
FILE* p_Write = fopen("contact.txt","wb");
if (p_Write == NULL)
{
perror("saveContact");
return;
}
// 写文件
int i = 0;
for(i = 0;i < p->count; i++)
{
fwrite(p->list + i, sizeof(PeoInfo), 1, p_Write);
}
// 关闭文件
fclose(p_Write);
p_Write = NULL;
}
修改3:下次启动程序,需要读取文件,修改初始化函数,加载文件信息到通讯录中。
这里我们可以封装成一个函数LoadContact,这里读取的时候需要判断通讯录是否放得下,放得下就放,放不下就扩容;
注意fread这里的返回值是读取到元素的个数,这里每次读取一个,如果有一次读取到0了,说明已经读取完毕了。
cpp
void LoadContact(Contact* p)
{
FILE* p_Read = fopen("contact.txt","rb");
if (p_Read == NULL)
{
perror("LoadContact");
}
PeoInfo tmp = { 0 };
while (fread(&tmp, sizeof(PeoInfo), 1, p_Read) != 0)
{
if (p->count == p->capacity)
{
addContact(p);
}
p->list[p->count] = tmp;
p->count++;
}
fclose(p_Read);
p_Read = NULL;
}
// 动态初始化
void InitContact(Contact* p)
{
assert(p);
p->count = 0;
// 分配空间并且赋初值
p->list = (PeoInfo*)calloc(3, sizeof(PeoInfo));
if (p->list == NULL)
{
printf("%s\n", strerror(errno));
}
p->capacity = 3;
// 加载文件信息到通讯录中
LoadContact(p);
}
测试:
此时这里有4个联系人,终止程序会存入本地。

启用程序之后直接查看通讯录:这里我们故意将文件联系人数量 > 通讯录的初始容量,测试扩容是否能够实现。

5.1 通讯录(动态内存 + 可持久化版)完整代码
注意:运行之前contact.txt必须存在当前路径下!
contact.h
cpp
#define _CRT_SECURE_NO_WARNINGS
#define N 100
#define NAME_LONG 20
#define GENDER_LONG 10
#define TELE_LONG 12
#define ADDRESS_LONG 30
#include<string.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
// 人信息的结构体声明
typedef struct PeoInfo
{
char name[NAME_LONG]; // 姓名
int age; // 年龄
char gender[GENDER_LONG]; // 性别
char tele[TELE_LONG]; // 电话号码
char addr[ADDRESS_LONG]; // 地址
}PeoInfo;
// 通讯录结构体的声明
//typedef struct Contact
//{
// // 通讯录假如可以存放一百条信息
// PeoInfo list[N];
// // 真实存放的信息的条数
// int count;
//}Contact;
// 动态版本
typedef struct Contact
{
// 通讯录假如可以存放一百条信息
PeoInfo* list;
// 真实存放的信息的条数
int count;
// 动态空间的容量
int capacity;
}Contact;
// 初始化通讯录结构体变量
void InitContact(Contact* p);
// 通讯录的增加方法
void addContact(Contact* p);
// 通讯录的显示
void showContact(const Contact* p);
// 通讯录的删除
void deleContact(Contact* p);
// 通讯录的查找
void SearchContact(Contact* p);
// 通讯录的修改
void ModifyContact(Contact* p);
// 通讯录的排序
void SortContact(Contact* p);
// 通讯录数据销毁
void destroyMem(Contact* p);
// 通讯录数据持久化
void saveContact(const Contact* p);
contact.c
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"contact.h"
void plusCapacity(Contact* p)
{
if (p->count == p->capacity)
{
// 扩容,每次多2
PeoInfo* tmp = realloc(p->list, (p->capacity + 2) * sizeof(PeoInfo));
if (tmp == NULL)
{
printf("%s\n", strerror(errno));
return;
}
p->list = tmp;
p->capacity += 2;
printf("扩容成功!\n");
}
}
void LoadContact(Contact* p)
{
FILE* p_Read = fopen("contact.txt","rb");
if (p_Read == NULL)
{
perror("LoadContact");
}
PeoInfo tmp = { 0 };
while (fread(&tmp, sizeof(PeoInfo), 1, p_Read) != 0)
{
if (p->count == p->capacity)
{
plusCapacity(p);
}
p->list[p->count] = tmp;
p->count++;
}
fclose(p_Read);
p_Read = NULL;
}
// 动态初始化
void InitContact(Contact* p)
{
assert(p);
p->count = 0;
// 分配空间并且赋初值
p->list = (PeoInfo*)calloc(3, sizeof(PeoInfo));
if (p->list == NULL)
{
printf("%s\n", strerror(errno));
}
p->capacity = 3;
// 加载文件信息到通讯录中
LoadContact(p);
}
// 通讯录的增加方法
void addContact(Contact* p)
{
assert(p);
plusCapacity(p);
printf("请输入姓名:\n");
scanf("%s", (p->list)[p->count].name);
printf("请输入年龄:\n");
scanf("%d", &((p->list)[p->count].age));
printf("请输入性别:\n");
scanf("%s", (p->list)[p->count].gender);
printf("请输入电话号码:\n");
scanf("%s", (p->list)[p->count].tele);
printf("请输入地址:\n");
scanf("%s", (p->list)[p->count].addr);
(p->count)++;
printf("通讯录增加成功!\n");
}
// 通讯录的显示
void showContact(const Contact* p)
{
assert(p);
int i = 0;
printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n"
, "姓名", "年龄", "性别", "电话", "地址"
);
for (i = 0; i < p->count; i++)
{
printf("%-20s\t%-5d\t%-5s\t%-12s\t%-30s\n",
(p->list)[i].name,
(p->list)[i].age,
(p->list)[i].gender,
(p->list)[i].tele,
(p->list)[i].addr);
}
}
static int findByname(const Contact* p, char name[])
{
assert(p);
int i = 0;
for (i = 0; i < p->count; i++)
{
if (strcmp(p->list[i].name, name) == 0)
{
return i;
}
}
return -1;
}
// 通讯录的删除
void deleContact(Contact* p)
{
char name[NAME_LONG] = { 0 };
assert(p);
// 判断通讯录联系人数量为0
if (p->count == 0)
{
printf("通讯录为空,无法删除\n");
}
printf("请输入要删除的联系人姓名\n");
scanf("%s", name);
int ret = findByname(p, name);
if (ret == -1)
{
printf("删除的联系人不存在!\n");
return;
}
else
{
// 删除
for (int i = ret; i < p->count - 1; i++)
{
p->list[i] = p->list[i + 1];
}
p->count--; // 有效的数据-1
printf("删除成功!!\n");
}
}
// 通讯录的查找
void SearchContact(Contact* p)
{
char name[NAME_LONG];
printf("请输入查找的姓名:\n");
scanf("%s", name);
int ret = findByname(p, name);
if (ret == -1)
{
printf("该姓名不存在!\n");
return;
}
else
{
printf("查找成功!以下是该联系人的信息:\n");
printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n"
, "姓名", "年龄", "性别", "电话", "地址"
);
printf("%-20s\t%-5d\t%-5s\t%-12s\t%-30s\n",
(p->list)[ret].name,
(p->list)[ret].age,
(p->list)[ret].gender,
(p->list)[ret].tele,
(p->list)[ret].addr);
}
}
// 通讯录的修改
void ModifyContact(Contact* p)
{
char name[NAME_LONG];
printf("请输入要修改的联系人姓名:\n");
scanf("%s", name);
int ret = findByname(p, name);
if (ret == -1)
{
printf("该姓名不存在!\n");
return;
}
else
{
printf("请输入修改姓名:\n");
scanf("%s", (p->list)[ret].name);
printf("请输入修改年龄:\n");
scanf("%d", &((p->list)[ret].age));
printf("请输入修改性别:\n");
scanf("%s", (p->list)[ret].gender);
printf("请输入修改电话号码:\n");
scanf("%s", (p->list)[ret].tele);
printf("请输入修改地址:\n");
scanf("%s", (p->list)[ret].addr);
printf("通讯录修改成功!\n");
}
}
size_t contact_cmp(const void* e1, const void* e2)
{
return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);
}
// 通讯录的排序
void SortContact(Contact* p)
{
assert(p);
qsort(p->list, p->count, sizeof(p->list[0]), contact_cmp);
printf("排序成功!\n");
}
// 通讯录数据销毁
void destroyMem(Contact* p)
{
if (p == NULL)
{
printf("%s\n",strerror(errno));
}
free(p->list);
p->list = NULL;
}
// 通讯录数据持久化
void saveContact(const Contact* p)
{
assert(p);
FILE* p_Write = fopen("contact.txt","wb");
if (p_Write == NULL)
{
perror("saveContact");
return;
}
// 写文件
int i = 0;
for(i = 0;i < p->count; i++)
{
fwrite(p->list + i, sizeof(PeoInfo), 1, p_Write);
}
// 关闭文件
fclose(p_Write);
p_Write = NULL;
}
test.c
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"contact.h"
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
void menu()
{
printf("========================================\n");
printf("======1.add 2.del ========\n");
printf("======3.search 4.modify ========\n");
printf("======5.show 6.sort ========\n");
printf("======0.exit ========\n");
printf("========================================\n");
}
int main()
{
// 声明通讯录结构体变量
Contact con;
// 初始化通讯录结构体变量
InitContact(&con);
int input = 0;
do
{
menu();
printf("请输入一个选项:\n");
scanf("%d", &input);
switch (input)
{
case ADD:
addContact(&con);
break;
case DEL:
deleContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SHOW:
showContact(&con);
break;
case SORT:
SortContact(&con);
break;
case EXIT:
printf("程序已退出!\n");
saveContact(&con);
destroyMem(&con);
break;
default:
printf("请正确输入选项!\n");
break;
}
} while (input);
return 0;
}
6. 文件的随机读写
之前我们所有的文件读写,都是按照顺序读的,例如fgetc每次只能读取一个字符,接下来我们要介绍的函数可以指定位置随机读写。
6.1 fseek函数
根据文件指针的位置以及偏移量来定位文件指针。
cpp
int fseek(FILE* stream,long int offset,int origin);

这里有三个常量,分别代表文件指针的三个位置:起始位置、当前位置、最后的位置。
具体使用如下:
cpp
#include<stdio.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt", "r");// abcdef
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 文件指针定位:从起始位置偏移两个位置即c
fseek(pf, 2, SEEK_SET);// c
int ch = getc(pf);
printf("%c\n",ch);
// 从d开始向后偏移两个位置即f
fseek(pf, 2, SEEK_CUR);// f
ch = getc(pf);
printf("%c\n",ch);
// 从末尾开始向前偏移1即f
fseek(pf, -1, SEEK_END);// f
ch = getc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}

6.2 ftell
返回文件指针相对于起始位置的偏移量。
cpp
long int ftell(FILE* stream);
cpp
#include<stdio.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt", "r");// abcdef
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 文件指针定位:从起始位置偏移两个位置即c
fseek(pf, 2, SEEK_SET);// c
int ch = getc(pf);
printf("%c\n",ch);
printf("%d\n",ftell(pf)); // 输出完毕之后指针在d,相对起始位置偏移3
// 从d开始向后偏移两个位置即f
fseek(pf, 2, SEEK_CUR);// f
ch = getc(pf);
printf("%c\n",ch);
printf("%d\n", ftell(pf)); // 输出完毕之后指针在f之后,相对起始位置偏移6
// 从末尾开始向前偏移1即f
fseek(pf, -1, SEEK_END);// f
ch = getc(pf);
printf("%c\n", ch);
printf("%d\n", ftell(pf)); // 输出完毕之后指针在f之后,相对起始位置偏移6
fclose(pf);
pf = NULL;
return 0;
}

6.3 rewind函数
此时上面的指针走啊走,不知道去了哪里,这时候需要rewind函数将指针指向起始位置。
cpp
#include<stdio.h>
int main()
{
FILE* pf = fopen("E:\\DeskTop\\test.txt", "r");// abcdef
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 文件指针定位:从起始位置偏移两个位置即c
fseek(pf, 2, SEEK_SET);// c
int ch = getc(pf);
printf("%c\n",ch);
printf("%d\n",ftell(pf)); // 输出完毕之后指针在d,相对起始位置偏移3
// 从d开始向后偏移两个位置即f
fseek(pf, 2, SEEK_CUR);// f
ch = getc(pf);
printf("%c\n",ch);
printf("%d\n", ftell(pf)); // 输出完毕之后指针在f之后,相对起始位置偏移6
// 从末尾开始向前偏移1即f
fseek(pf, -1, SEEK_END);// f
ch = getc(pf);
printf("%c\n", ch);
printf("%d\n", ftell(pf)); // 输出完毕之后指针在f之后,相对起始位置偏移6
// 指针归零
rewind(pf);
ch = getc(pf);
printf("%c\n", ch); // a
fclose(pf);
pf = NULL;
return 0;
}

7. 文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件 或者二进制文件。
①数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
②如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
7.1 一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

这两种方式没有绝对的优劣,如果存数字1,采用二进制存储需要四个字节,采用ASCII码存储只需要1个字节(本质上将每一位数字当做一个字符进行存储)。
使用ASCII码进行存储就是文本格式,使用二进制进行存储就是二进制格式,如下图所示:

举一个例子:

正常二进制写入文件,再用二进制格式打开文件:这里就和上面的图遥相呼应了,使用4个字节进行存储(小端存储)。

8. 文件读取结束的判定
8.1 被错误使用的feof
牢记 :在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束 。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1. 文本文件读取是否结束 ,判断返回值是否为EOF(fgetc)或者NULL(fgets)
例如:
fgetc判断是否为EOF.
fgets判断返回值是否为NULL
2.二进制文件的读取结束判断 ,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数
形参中的count是实际读取的元素的个数,如果返回值小于这个个数,说明已经读完了。

举一个例子:fp这里可能为NULL(就是0),需要进行判断是否为空指针;fgetc读取失败的试试会返回EOF,这里有两种可能,一种是读完了,一种是其他的错误。


使用ferror来判断错误,如果是其他错误就输出;feof判断文件读取是否到结尾了,若已经读取完毕了,继续输出相应信息。
9.文件缓冲区
ANSIC标准采用"缓冲文件系统"处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

文件缓冲区的设立就是为了提高效率,减少由于频繁地IO带来的损耗。
证明缓冲区的存在:
当程序运行的时候,写入文件会停留10秒,此时打开文件,发现没有内容;刷新缓冲区之后,文件才会有内容,fclose也会刷新缓冲区,所以在此之前需要停留10秒避免造成逻辑混乱。
