【C语言】简易通讯录3及文件操作介绍

前言

关于静态通讯录的实现【C语言】简易通讯录的实现

关于动态通讯录的实现【C语言】简易通讯录2及动态内存管理介绍

上一次我们对我们的静态通讯录进行了改造,让它的容量不再固定,那么在这一次我们将解决最后一个问题:

  • 它无法在程序退出后保存数据,每一次进入程序都要重新记录数据

那么在这一次我们将借用C语言中文件操作的知识,来帮助我们实现通讯录数据继承问题

那么我们有必要先来了解一下帮助我们实现这个功能的一些知识

文件操作

文件名

为了能够操作到我们所需的文件,我们要知道每个文件特有的文件标识——文件名

实际上,在下面的方法介绍中,文件名包含3个部分 文件路径+文件名主干+文件后缀. 这和我们日常生活中所认为的文件名不太一样, 这是为了能够区分不同路径的文件名

实际上我们日常生活中指的文件名就是文件名主干文件名主干+文件后缀,但实际上文件路径也被算作是文件名的一部分

但是注意:在一些操作系统或者程序中,也可能存在不需要或者不允许使用文件后缀名的情况,例如一些特殊的文件,或者是一些自定义的文件类型等。因此,在一些场景下,文件名并不一定要包含文件后缀。

文件的打开和关闭

回忆一下我们自己手动操作一些文件的过程,是不是要先把文件打开才能操作里面的内容,并且在操作完了之后要把文件关闭

因此为了能在程序中操作文件,我们肯定要先让程序打开文件,并且在最后关闭文件

那么C语言中也提供了这两个函数

打开文件

FILE* fopen (const char* filename, const char* mode );

文件打开成功返回文件类型指针(见下面介绍),失败则返回空指针NULL

关闭文件

int fclose (FILE* stream);

文件关闭成功返回0,失败则返回EOF(-1)


其中我们看到有一个FILE*,这个就是C语言为了文件操作而定义的一种指针类型,称为**文件类型指针**,一般我们将它简称为文件指针

当我们访问文件的时候,文件会在内存维护一块空间,称为文件信息区,这个区域存储着有关于我们操作的这个文件的各种信息。而这个区域实际上就是由结构体变量开辟的,这个结构体的类型就是FILE。在标准库中,FILE结构体包含了与文件相关的信息,如文件的位置指针,缓冲区指针,缓冲区大小等。具体结构体定义可能会因操作系统和编译器的不同而有所不同,但它们通常都会包含这些基本的成员变量。

在打开文件的时候,系统会自动根据文件情况创建一个FILE的结构体变量,也就是开辟一块文件信息区。而我们的文件指针指向的就是这块区域,同时程序也通过这个区域存储的信息来访问并操作文件。

注意区分文件的位置指针文件类型指针,这两个指针都可以被称为文件指针,但是文件类型指针指向的是文件信息区,而文件的位置指针是指向文件内部的信息,便于我们在对应的位置输入/输出信息。

并且,由于在大多数时候,我们即便是在操作文件位置指针,但实际上传给函数的也是文件类型指针,所以很多时候我们也不对其进行细致的区分,都直接称其为文件指针


剩下的参数我们依次介绍

filename:文件名,如果要操作的文件就在工程文件夹内部,则只用写文件名主干+文件后缀,但是不在工程文件夹内部的文件就要使用完整的文件名,书写文件路径的时候可以使用转义字符\去抵消一些转义字符,例如

FILE* pf = fopen("C:\\Users\\ShmentLife\\Desktop\\test\\test.txt", "r");

mode:打开文件的方式,这个会影响我们后续对于文件的操作,常见的打开方式见下表

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

暂时我们先熟悉前面三个即可

那么有些人可能看前面三个含义有些疑问,读、写、输入、输出是相对谁而言的?

因为执行文件操作的是我们写的程序,因此我们的所有读、写、输入、输出都是相对程序而言的,用一张图来表示就是

在这里插入图片描述


了解了上面的知识,我们就来看一个示例

int main() {
    
    
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	//判断成功与否,防止非法访问
	if (pf == NULL) {
    
    
		perror("fopen");
		return 1;
	}
	//文件操作

	//...

	//关闭和置空指针
	fclose(pf);
	pf = NULL;
	return 0;
}

同时,我们需要注意一下"w" "a"的区别,"w" 每次都是创建一个新的文件,而"a"是在之前有的文件基础上继续追加

虽然两个倘若在没有指定文件的情况下,都是创建一个新的,但是对于已有文件的处理方式有所不同


接下来我们就学习一下如何操作文件

文件的顺序读写

C语言提供了一些函数,让我们能够对文件进行输出或输入,我们先介绍顺序读写的函数

什么是顺序读写,就是我们无法改变读写的次序,只能从前往后

以下就是一些顺序读写函数

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件

那么可能有人会疑惑,这里的适用于所有输出流 所有输入流是什么意思,这就涉及到了流的概念

流的概念

流实际上就类似于一个位于程序和输出/输入端中间的一个中转站,那么为什么需要这个中转站?

在我们书写程序的时候,有时会有一些从外部输入/输出到外部的需求,那么如果没有流这个中转站,程序员在书写程序的时候,就要考虑到:怎么从外部读取?怎么输出到外界?类似于这样的问题

但是,流这个中转站就可以帮助程序员不必去担心这类问题,我们书写程序的时候就只用输入/输出到流内,那么实际上怎么输入输出(也就是那些设备/文件怎么从流中读取数据并输入/输出),就不是我们写程序时候要考虑的问题

简单地说,就是我们书写程序的时候,流可以帮助把我们的输入和输出数据抽象化,将程序与底层设备的细节分离开来,使程序更加通用和可移植。例如,程序可以在不同的操作系统或硬件平台上运行,只需要修改对应的流操作即可,而不需要修改程序的核心逻辑。


在上面我们提到了FILE*,它其实就是一个流,为什么?因为我们在对文件进行输入输出的时候,我们就只用操作这个流,而不用考虑到底是怎么输出/输入到文件里的,这个流就被称作为文件流

在使用这个流的时候,我们可以对这个流指定访问模式,例如只读、只写、追加等等,同时我们可以通过各种函数(就是我们下面要讲的那些函数)对这个流进行操作,从而达到对文件进行输入输出的效果

另外,C语言标准中还定义了三个特殊的流stdin (标准输入流)stdout(标准输出流) stderr(标准错误输出流),这三个流被定义为标准流,在程序启动的时候会自动启用。因此可以不用像上面的文件流一样,是要事先打开或创建的

实际上之前我们就已经多次使用过这些标准流,例如我们使用printf的时候,之前我们说它会进行标准输出,实际上就是输出到标准输出流里,然后被我们的屏幕读取并显示(但是并不是说就一定通过屏幕输出,只不过通常是以屏幕输出)。scanf也是同理,我们的键盘的键入被看作是标准输入,被放于标准输入流中,然后scanf去读取标准输入流的数据

fgetc

fgetc用于从文件中读取字符,其定义如下

int fgetc ( FILE * stream );

返回值:类型为int,返回的是读取字符的ASCII码值,如果读取失败或读取到文件末尾则会返回EOF

参数介绍:stream,文件指针,指向我们要读取文件的文件信息区

假设我们的test.txt文件内存储了以下信息

abcdef

那么我们就可以通过下面的代码读取字符

#include<stdio.h>
int main() {
    
    
    //这里的文件打开方式要能够满足我们的使用方式
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL) {
    
     
		perror("Error opening file");
        return 0;
	}
    
	printf("%c", fgetc(pf));

	fclose(pf);
	pf = NULL;
	return 0;
}

那么我们假如想要读取后面的字符,在不使用其他函数的情况下能不能做到呢?有些人可能就想,那么我让pf++,不就行了吗?

但是这是万万不可的,上面我们说过,pf作为一个FILE*类型,指向的是存储着文件信息的文件信息区,如果你这个时候让pf向后跳过一个FILE类型大小,那么就会到达未知的内存区域,结果就是造成非法访问

实际上,我们想要访问后面的字符,直接多次使用fgetc即可,因为在第一次使用这个函数的时候,FILE类型内部的文件位置指针会自动指向文件中的第一个字符,并且当我们每一次使用这个函数,这个指针就会自动往后跳过一个字符

因此我们若想要读取文件中的所有字符,直接让fgetc执行 6次即可

#include<stdio.h>
int main() {
    
    
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL) {
    
     
		perror("Error opening file");
        return 0;
	}

	for (int i = 0; i < 7; i++) {
    
    
		printf("%c", fgetc(pf));
	}
	
	fclose(pf);
	pf = NULL;
	return 0;
}

倘若此时循环次数大于 6,fgetc函数在读取到文件末尾时会遇到EOF,不再继续往后读

fputc

fputc用于输出字符,将字符写入文件,定义如下

int fputc ( int character, FILE * stream );

返回值:类型为int,返回的是输出字符的ASCII码值,如果输出失败则会返回EOF

参数介绍:character为要输出字符的ASCII码值,代表我们要输出的字符

stream,文件指针,指向我们要读取文件的文件信息区

基本上性质和fgetc相同,区别就是一个输入一个输出,所以直接上代码

int main(){
    
    
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL) {
    
     
		perror("Error opening file");
         return 0;
	}
	//输出26个字母到文件里
	for (int i = 0; i < 26; i++) {
    
    
		fputc('a' + i, pf);
	}

	fclose(pf);
	pf = NULL;
	return 0;
}

fgets

可以理解为fgetc的升级版,可以一次性读多个字符(也就是字符串),定义如下

char * fgets ( char * str, int num, FILE * stream );

返回值:char*,如果函数正常运行返回的就是参数中的str,如果失败就会返回NULL指针

参数介绍:str,读取的字符串被粘贴到的位置

numfgets会读取文件中num-1个字符,其中第num个字符用来放\0

stream,文件指针,指向我们要读取文件的文件信息区

使用上面用fputc写了26个字母的文件来帮助执行这段代码

int main() {
    
    
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL) {
    
    
		perror("Error opening file");
        return 0;
	}

	char str[30];

	printf("%s", fgets(str, 27, pf));
	
	fclose(pf);
	pf = NULL;
	return 0;
}

fputs

fgets差不多,可以当作fputc的升级版本,直接输出字符串,定义如下

int fputs ( const char * str, FILE * stream );

返回值:如果输出成功返回非负整数,失败返回EOF

参数介绍:str,指向你要输出的字符串

stream,文件指针,指向我们要读取文件的文件信息区

int main() {
    
    
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL) {
    
    
		perror("Error opening file");
		return 0;
	}

	char str[] = "abcdef";

	fputs(str, pf);

	fclose(pf);
	pf = NULL;
	return 0;
}

fscanf

一个按照格式读取文件中数据的函数,具体使用方式可以参考scanf

int fscanf ( FILE * stream, const char * format, ... );

其实就是和scanf差不多,只不过scanf只是针对标准输入流的函数,而这个是针对所有流的函数,当然也就包括文件流

使用方式,除了要提供一个文件指针外,和scanf毫无区别,把文件的数据想象成你键盘输入的数据就可以无缝衔接的使用这个函数

假设我文件内部存储着下列数据

15 male xiaoming

那么用一个结构体变量来接收就可以写成下列代码

struct person
{
    
    
	int age;
	char gender[10];
	char name[20];
};

int main(){
    
    
	struct person p1;
	FILE* pf = fopen("test.txt", "r");
	if (NULL == pf) {
    
    
		perror("fopen");
	}
	fscanf(pf, "%d %s %s", &(p1.age), p1.gender, p1.name);

	printf("%d %s %s", p1.age, p1.gender, p1.name);
    fclose(pf);
    pf = NULL;
	return 0;
}

fprintf

这个自然也就不用多说了,和上面的fscanfscanf的性质相同

printf是针对标准输入流的函数,而这个是针对所有流的函数,当然也就包括文件流,定义如下

int fprintf ( FILE * stream, const char * format, ... );

也同样的,可以将文件存储想象为屏幕,可以更好的理解这个函数

struct person
{
    
    
	int age;
	char gender[10];
	char name[20];
};

int main(){
    
    
	struct person p1 = {
    
    20, "female", "xiaomei"};
	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf) {
    
    
		perror("fopen");
	}
	fprintf(pf, "%d %s %s", p1.age, p1.gender, p1.name);
	fclose(pf);
	pf = NULL;
	return 0;
}

上面我们就讲解了一些适用于所有输出流的函数,那么既然是适用于所有输出流的函数,其实也可以直接使用标准流,而不一定要用文件流

如下

int main() {
    
    
	char str[] = "hehe";
    //直接输出到标准输出流
	fputs(str, stdout);
	return 0;
}
int main() {
    
    
	char str[20];
    //直接从标准输入流读取
	fgets(str,6,stdin);
	printf("%s", str);
	return 0;
}

下面再看两个适用于文件流的函数

fwrite

这个函数用于以二进制的数据格式向文件输出信息,而不是像上面的几个函数是以文本格式输出,定义如下

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

返回值:返回成功输出的元素的个数

参数介绍:ptr,指向你要输出的数据的地址

size,输出的数据中,单个元素的大小

count,要输出的元素个数

stream,文件指针,指向我们要读取文件的文件信息区

typedef struct person
{
    
    
	int age;
	char gender[10];
	char name[20];
}person;
int main() {
    
    
	person p1 = {
    
     10,"male","zhangsan"};
	FILE* pf = fopen("test.txt","wb");
	if (pf == NULL) {
    
    
		perror("fopen");
        return 1;
	}
	fwrite(&p1, sizeof(p1), 1, pf);
	fclose(pf);
	pf = NULL;
}

那么这样我们就往文件内存储了一些二进制数据,那么此时我们会发现我们直接以文本形式打开里面就是一些乱码,此时我们为了能够读取里面的数据,可以用二进制的形式去读取数据,那么下面这个fread函数久提供了相应的功能

fread

这个函数用于以二进制的数据格式向文件读取信息,定义如下

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

返回值:返回成功读取的元素的个数,读到EOF返回0

参数介绍:ptr,指向你读取的数据之后被存储的位置,大小应该至少为size*count

size,读取的数据中,单个元素的大小

count,要读取的元素个数

stream,文件指针,指向我们要读取文件的文件信息区

typedef struct person
{
    
    
	int age;
	char gender[10];
	char name[20];
}person;
int main() {
    
    
	person p1;
	FILE* pf = fopen("test.txt","rb");
	if (pf == NULL) {
    
    
		perror("fopen");
        return 1;
	}
	fread(&p1, sizeof(p1), 1, pf);
	printf("%d %s %s", p1.age, p1.gender, p1.name);
	fclose(pf);
	pf = NULL;
}

文件的随机读写

说是随机读写,但实际上并不是完全随机。实际上就是能够让我们不像上面的几个函数一样只能按照顺序读写,而是可以通过控制文件位置指针的指向位置,读取目标数据

fseek

fseek用于在打开的文件中移动文件位置指针,从而访问我们想要的数据,而不是只能通过顺序读写,定义如下

int fseek ( FILE * stream, long int offset, int origin );

返回值:如果成功移动则会返回0,失败则返回非0

参数介绍:stream,文件指针,指向我们要读取文件的文件信息区

offset,文件位置指针的移动距离(单位为字节),如果是向左偏移n位就传-n,向右移动n就传n

origin,偏移量offset参考的起始点,一般常用的参数有如下三个

参数名 参数意义
SEEK_SET 文件开头
SEEK_CUR 文件位置指针当前所处的位置
SEEK_END 文件末尾

假设文件中存储着如下的信息

abcdef
#include<stdio.h>
int main() {
    
    
	FILE* pf = fopen("test.txt","r");
	if (NULL == pf) {
    
    
		perror("fopen");
		return 1;
	}
	//顺序读4个字符,此时指向e
	for (int i = 0;i < 4;i++) {
    
    
		fgetc(pf);
	}
	//让文件指针向左偏移三个
	fseek(pf, -3, SEEK_CUR);
	//这个时候下面就会读字符b
	printf("%c", fgetc(pf));
	fclose(pf);
	pf = NULL;
	return 0;
}

ftell

一个告知我们当前文件位置指针相对于文件起点的偏移量的函数,定义如下

long int ftell ( FILE * stream );

过于简单,不多阐述

rewind

一个将文件位置指针重新归位到文件起点的函数,定义如下

void rewind ( FILE * stream );

过于简单,不多阐述

文件读取结束的判定

这里很多资料上都说可以用feof来判断文件位置指针是否读取到了EOF,也就是拿它来判断文件是否读取结束。但实际上并非如此,我们先了解一下feof这个函数

feof

用来在读取文件操作完成后,检测EOF指示器

int feof ( FILE * stream );

返回值:当检测到EOF指示器的时候,返回非负数,其他情况返回0

参数介绍:stream,文件指针,指向我们要读取文件的文件信息区


什么是EOF指示器?

当我们文件正常读取到文件末尾的时候,会读到EOF,那么此时程序会设定一个**end-of-file indicator**,也就是EOF指示器

当文件不正常结束,比如读取失败后结束,那么此时程序会设定一个**error indicator**,也就是错误指示器,这个错误指示器可以用ferror函数检测,ferror除了检测的东西不一样剩余的参数和返回值都是类似的


综上,feof是用来检测EOF指示器的,而不是用来检测EOF的。并且EOF指示器只会在文件读取操作完成后设置,也就是说feof只能用于文件读取结束后,用来判断是正常读到EOF结束还是由于读取失败结束

下面举一些正确使用feof的例子

fgetcfeof

由于fgetc在读取失败和读取到文件末尾的时候,返回的都是EOF,所以这个时候通过检测是否有EOF指示器来判断文件读取是否正常结束

int main() {
    
    
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL) {
    
    
		perror("fopen");
		return 1;
	}
	while (fgetc(pf) != EOF) {
    
    
	}
    //判断哪一个指示器被设立了
	if (feof){
    
    
		printf("读取成功\n");
	}
	else if (ferror) {
    
    
		printf("读取失败\n");
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
fgetsfeof

由于fgets在读取到文件末尾和读取失败的时候都是返回NULL指针,因此也需要通过指示器判断是否正常结束

int main() {
    
    
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL) {
    
    
		perror("fopen");
		return 1;
	}
	char tmp[20];
	while (fgets(tmp, 3, pf) != NULL) {
    
    
	}
    //判断哪一个指示器被设立了
	if (feof) {
    
    
		printf("读取成功\n");
	}
	else if (ferror) {
    
    
		printf("读取失败\n");
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
freadfeof

当我们让fread去读取数据的时候,文件中数据不足的话返回值会小于我们要求要读的数据数量,但读取失败也会导致返回值小于我们要求要读的数据数量,因此此时依旧需要通过feof来判断是因为读到结尾结束还是读取失败结束

int main() {
    
    
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL) {
    
    
		perror("fopen");
		return 1;
	}
    //假设文件中存的就是 tmp存的数据
	char tmp[] = "abdbfbfhja";
	int count = sizeof(tmp) + 5;
	int test = fread(tmp, 1, count, pf);
	if (count == test) {
    
    
		printf("读取正常\n");
	}
	else {
    
    
		printf("读取异常,开始检测异常\n");
		if (feof) {
    
    
			printf("读取到EOF\n");
		}
		else if (ferror) {
    
    
			printf("文件读取失败\n");
		}
	}

	fclose(pf);
	pf = NULL;
	return 0;
}

通讯录数据存储的实现

上面讲了铺垫了那么多,那么我们就可以正式开始实现我们通讯录的最终版本

我们核心用到的是两个函数freadfwrite因为它的参数是最适合我们结构体的存储的

实际上我们就只用加入两个功能:1. 销毁前存储数据 2. 初始化时加载数据

存储数据

既然是将数据存储起来,那么肯定用到的是我们的fwrite函数

同时,我们这里选择将数据一次一次的存入文件中

void SaveContact(Contact* p) {
    
    
	FILE* pf = fopen("data.dat", "wb");
	if (NULL == pf) {
    
    
		perror("Save error");
		return;
	}
	for (int i = 0; i < p->dataNum; i++) {
    
    
		fwrite(p->data + i, sizeof(Peopleinfo), 1, pf);
	}
	fclose(pf);
	pf = NULL;
}

如果想一次性全部存入文件里,可以将存储的代码修改为以下代码

fwrite(p->data, sizeof(Peopleinfo)*(p->dataNum), 1, pf);

读取数据

读取数据要考虑的因素就比较多了

首先我们肯定不可能一次性将所有数据读取,因为我们刚初始化,根本不知道有多少个数据。其次,我们的初始容量有限,要一边加数据并且一边判断是否要扩容。最后,还要随着数据的改变增加dataNum(即有效数据数量)

当然你可以选择在存储数据的时候保存一份dataNum,我们这里主要讨论没有存dataNum的情况

我们首先要确定文件的打开方式,考虑到如果第一次使用的话,是没有数据的,那么就要创建一个文件。其次我们不能让程序在后续使用的时候把我们的文件清空,那么就要用到"ab+"的读写方式。但是如果使用这个读写方式,初始的时候文件位置指针指向的是文件的末尾,那么我们就再用一个rewind让它回到文件起点

FILE* pf = fopen("data.dat", "ab+");
rewind(pf);

然后就是让fread一个个的读取数据,那么肯定要用到循环,那循环的判断应该怎么写呢?

当我们的fread读不到数据的时候就应该停止循环,根据我们上面的知识就可以知道fread如果读到文件末尾就不会继续读了,此时代表着读取数据数量的返回值就会是0,因此我们可以直接将fread的返回值作为循环判断的条件

同时,我们不能在fread中直接将读取到的数据放到结构体指向的空间里,为什么呢?

举一个例子:假如我们文件中存储的数据数量大于3,当我们已经加了3个元素的时候,那么此时dataNum等于3,代码走了3轮,已经达到了初始的最大存储量,理论上要进行扩容才能放数据。但是由于我们直接将数据的存放放在了循环的判断里,那么这个时候这个代码就要将第四组数据放入空间才会运行扩容代码,那么就产生了两个致命的问题:

  1. 操作了未定义的空间,是非法访问
  2. 内存操作冲突了realloc函数扩容的时候会遇到我们操作过的空间

那么这个时候就会造成栈区的损坏

因此整个代码写下来应该是这样的

int LoadContactData(Contact* p)
{
    
    
    //打开文件
	FILE* pf = fopen("data.dat", "ab+");
	rewind(pf);
	if (NULL == pf) {
    
    
		perror("Load failure");
		return 1;
	}
    //拿取数据
	Peopleinfo tmp = {
    
     0 };
	int i = 0;
	while (i = fread(&tmp, sizeof(Peopleinfo), 1, pf)) {
    
    
        //判断
		if (!i) {
    
    
			if (!feof(pf)) {
    
    
				printf("数据读取出错\n");
				return 1;
			}
		}
        //扩容
		if (p->dataNum == p->capacity) {
    
    
			if (IncreaseContact(p)) {
    
    
				printf("扩容失败\n");
				return 1;
			}
		}
        //存放
		p->data[p->dataNum] = tmp;
		p->dataNum++;
	}
    //关闭文件和置空指针
	fclose(pf);
	pf = NULL;
	return 0;
}

总结

那么经历了三篇文章,我们的通讯录总算也是大体的完结了

实际上,写这个文件操作的通讯录部分就可以有很多种方法了:比如存储数据的时候存一份dataNum,读取的时候可以进行一些修改从而让我们在拿取数据的时候直接存放,我这里也只不过是分享我的思路,剩下的就靠各位读者自行研究了

代码总览

contact.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

static int IncreaseContact(Contact* p);

int LoadContactData(Contact* p)
{
    
    
	FILE* pf = fopen("data.dat", "ab+");
	rewind(pf);
	if (NULL == pf) {
    
    
		perror("Load failure");
		return 1;
	}
	Peopleinfo tmp = {
    
     0 };
	int i = 0;
	while (i = fread(&tmp, sizeof(Peopleinfo), 1, pf)) {
    
    
		if (!i) {
    
    
			if (!feof(pf)) {
    
    
				printf("数据读取出错\n");
				return 1;
			}
		}
		if (p->dataNum == p->capacity) {
    
    
			if (IncreaseContact(p)) {
    
    
				printf("扩容失败\n");
				return 1;
			}
		}
		p->data[p->dataNum] = tmp;
		p->dataNum++;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

int InitContact(Contact* p) {
    
    
	p->data = (Peopleinfo*)malloc(DEFAULT_SZ * sizeof(Peopleinfo));
	if (p->data == NULL) {
    
    
		printf("通讯录初始化失败:%s", strerror(errno));
		return 1;
	}
	p->dataNum = 0;
	p->capacity = DEFAULT_SZ;
	if (LoadContactData(p))
	{
    
    
		return 1;
	}
	return 0;
}

static int IncreaseContact(Contact* p) {
    
    
	Peopleinfo* ptr = (Peopleinfo*)realloc(p->data, (p->capacity + INCREACE_SZ) * sizeof(Peopleinfo));
	if (ptr == NULL) {
    
    
		printf("通讯录扩容失败:%s", strerror(errno));
		return 1;
	}
	p->capacity += INCREACE_SZ;
	p->data = ptr;
	return 0;
}

void AddContact(Contact* p) {
    
    

	if (p->dataNum == p->capacity) {
    
    
		if (IncreaseContact(p)) {
    
    
			printf("扩容失败\n");
			return;
		}
	}
	printf("接下来请按照指示输入各项数据\n");
	printf("请输入姓名\n");
	scanf("%s", (p->data)[p->dataNum].name);
	printf("请输入性别\n");
	scanf("%s", (p->data)[p->dataNum].gender);
	printf("请输入年龄\n");
	scanf("%d", &((p->data)[p->dataNum].age));
	printf("请输入电话\n");
	scanf("%s", (p->data)[p->dataNum].tele);
	printf("请输入地址\n");
	scanf("%s", (p->data)[p->dataNum].addr);
	p->dataNum++;
	printf("添加成功\n\n\n");
}

static void printContact(const Contact* p, int num) {
    
    
	printf("%-10s %-5s %-4d %-12s %-30s\n",
		(p->data)[num].name
		, (p->data)[num].gender
		, (p->data)[num].age
		, (p->data)[num].tele
		, (p->data)[num].addr);
}

void ShowContact(const Contact* p) {
    
    
	if (p->dataNum) {
    
    
		printf("%-10s %-5s %-4s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
		for (int i = 0; i < p->dataNum; i++) {
    
    
			printContact(p, i);
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}

static FindByName(const Contact* p, char name[MAX_NAME]) {
    
    
	for (int i = 0; i < p->dataNum; i++) {
    
    
		if (!strcmp(name, (p->data)[i].name)) {
    
    
			return i;
		}
	}
	return -1;
}

void SearchContact(const Contact* p) {
    
    
	if (p->dataNum) {
    
    
		char name[MAX_NAME] = {
    
     0 };
		printf("请输入要查找人的名字\n");
		scanf("%s", name);
		int i = FindByName(p, name);
		if (i != -1) {
    
    
			printf("查找成功\n");
			printf("%-10s %-5s %-4s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
			printContact(p, i);
		}
		else {
    
    
			printf("未查询到目标\n");
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}


void DelContact(Contact* p) {
    
    
	if (p->dataNum != 0) {
    
    
		char name[MAX_NAME] = {
    
     0 };
		printf("请输入要删除人的名字\n");
		scanf("%s", name);
		int pos = FindByName(p, name);
		if (pos != -1) {
    
    
			while (pos < p->capacity - 1) {
    
    
				p->data[pos] = p->data[pos + 1];
				pos++;
			}
			memset(&(p->data[pos]), 0, sizeof(p->data[pos]));
			p->dataNum--;
			printf("删除成功\n");
		}
		else {
    
    
			printf("要找的人不存在\n");
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}


void ModifyContact(Contact* p) {
    
    
	if (p->dataNum) {
    
    
		char name[MAX_NAME] = {
    
     0 };
		printf("请输入要修改数据的人名\n");
		scanf("%s", name);
		int i = FindByName(p, name);
		if (i != -1) {
    
    
			printf("接下来请按照指示输入各项数据\n");
			printf("请输入姓名\n");
			scanf("%s", (p->data)[i].name);
			printf("请输入性别\n");
			scanf("%s", (p->data)[i].gender);
			printf("请输入年龄\n");
			scanf("%d", &((p->data)[i].age));
			printf("请输入电话\n");
			scanf("%s", (p->data)[i].tele);
			printf("请输入地址\n");
			scanf("%s", (p->data)[i].addr);
			printf("修改成功\n\n\n");
		}
		else {
    
    
			printf("未查询到目标\n");
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}

static int SortByName(void* p1, void* p2) {
    
    
	return strcmp((char*)p1, (char*)p2);
}

void SortContact(Contact* p) {
    
    
	if (p->dataNum) {
    
    
		qsort(p->data, p->dataNum, sizeof(p->data[0]), SortByName);
		printf("排序成功\n\n");
		ShowContact(p);
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
		printf("\n\n\n");
	}
}

void DestroyContact(Contact* p) {
    
    
	free(p->data);
	p->dataNum = 0;
	p->capacity = 0;
	p->data = NULL;
}

void SaveContact(Contact* p) {
    
    
	FILE* pf = fopen("data.dat", "wb");
	if (NULL == pf) {
    
    
		perror("Save error");
		return;
	}
	for (int i = 0; i < p->dataNum; i++) {
    
    
		fwrite(p->data + i, sizeof(Peopleinfo), 1, pf);
	}
	fclose(pf);
	pf = NULL;
}

main.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

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");
}

enum Option
{
    
    
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SHOW,
	SORT
};

int main() {
    
    
	Contact con;
	if (InitContact(&con)) {
    
    
		return 0;
	}
	int input = 0;
	do {
    
    

		printf("请选择操作\n");
		menu();
		scanf("%d", &input);
		switch (input)
		{
    
    
		case ADD:
			AddContact(&con);
			break;
		case DEL:
			DelContact(&con);
			break;
		case SEARCH:
			SearchContact(&con);
			break;
		case MODIFY:
			ModifyContact(&con);
			break;
		case SHOW:
			ShowContact(&con);
			break;
		case SORT:
			SortContact(&con);
			break;
		case EXIT:
			SaveContact(&con);
			DestroyContact(&con);
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

contact.h

#define _CRT_SECURE_NO_WARNINGS 1


#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>

#define MAX_NAME 20
#define MAX_GENDER 5
#define MAX_TELE 12
#define MAX_ADDR 30
#define DEFAULT_SZ 3
#define INCREACE_SZ 2

typedef struct Peopleinfo
{
    
    
	char name[MAX_NAME];
	char gender[MAX_GENDER];
	int age;
	char tele[MAX_TELE];
	char addr[MAX_ADDR];
}Peopleinfo;

typedef struct Contact
{
    
    
	//存储有效数据个数
	int dataNum;

	//指向存贮数据的空间
	Peopleinfo* data;

	//记录最大容量
	int capacity;
}Contact;

//初始化通讯录
int InitContact(Contact* p);

//添加功能
void AddContact(Contact* p);

//展示通讯录
void ShowContact(const Contact* p);

//查找功能
void SearchContact(const Contact* p);

//删除功能
void DelContact(Contact* p);

//编辑通讯录
void ModifyContact(Contact* p);

//排序通讯录
void SortContact(Contact* p);

//销毁通讯录
void DestroyContact(Contact* p);

//保存通讯录
void SaveContact(Contact* p);

猜你喜欢

转载自blog.csdn.net/qq_42150700/article/details/130000429