C语言版通讯录
文章目录
主要功能
- 添加一个人员
- 打印显示所有人员
- 删除一个人员
- 查找一个人员
- 保存文件
- 加载文件
架构设计
我觉得这里对于我这个没有整体设计的菜鸟来说这个设计的方式真的让我眼前一亮。
主要的设计就是将程序分为三层支持层、接口层和业务逻辑层,每一层都只会使用其下面一层提供的功能,如业务层函数会调用接口层的函数,接口层的函数调用支持层的函数,不会让业务层直接调用支持层的函数。
源代码
/*
* 2022年6月7日10:29:41
* 通讯录管理系统
* 三层:支持层、接口层、业务逻辑层
* 双向链表作为数据结构
*
*
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//用宏定义让程序代码更直观
#define NAME_LENGTH 16
#define PHONE_LENGTH 32
#define BUFFER_LENGTH 128
//从文件中读出一行数据的最小值是5,即name:
#define MIN_TOKEN_LENGTH 5
//便于未来的改进,输出信息的函数更换只要在这修改就能整体修改
#define INFO printf
//用宏定义实现支持层的操作,要注意宏定义是直接替换
//将item插入list链表头部
//LIST_INSERT(ps, *ppeople)时这里list是*pperson,类型为(struct person *)直接替换
//要在将list加上括号等价于(*pperson)
#define LIST_INSERT(item, list) do{ \
item->prev = NULL; \
item->next = list; \
if((list) != NULL) (list)->prev = item; \
(list) = item; \
}while(0)
#define LIST_REMOVE(item ,list) do{ \
if(item->prev != NULL) item->prev->next = item->next; \
if(item->next != NULL) item->next->prev = item->prev; \
if(item == list) list = item->next; \
item->prev = item->next = NULL; \
}while(0) // 只执行一次
struct person
{
char name[NAME_LENGTH];
char phone[PHONE_LENGTH];
struct person *next;
struct person *prev; //设计的是双向链表,便于删除节点
};
//通讯录类,相当于时一个管理类
struct contacts
{
struct person *people; // 指向通讯录链表
int count;
};
enum {
OPER_INSERT = 1,
OPER_PRINT,
OPER_DELETE,
OPER_SEARCH,
OPER_SAVE,
OPER_LOAD
};
//define interface ,接口层会使用支持层的功能
int person_insert(struct person **ppeople, struct person *ps)
{
if(ps == NULL) return -1;
LIST_INSERT(ps, *ppeople);
return 0;
}
int person_delete(struct person **ppeople, struct person *ps)
{
if(ps == NULL) return -1;
LIST_REMOVE(ps, *ppeople);
return 0;
}
struct person * person_search(struct person *people, const char *name)
{
struct person *item = NULL;
for(item = people; item != NULL; item = item->next)
{
//strcmp相等为0
if(!strcmp(name, item->name))
break;
}
return item;
}
int person_traverse(struct person *people)
{
struct person *item = NULL;
for(item = people; item != NULL;item = item->next)
{
INFO("name: %s,phone: %s\n", item->name, item->phone);
}
return 0; // 成功返回0
}
//将通讯录(链表)people信息保存到文件filename中
int save_file(struct person *people, const char *filename)
{
FILE *fp = fopen(filename, "w");
if(fp == NULL) return -1;
struct person *item = NULL;
for(item = people; item != NULL; item = item->next)
{
fprintf(fp, "name: %s, phone: %s\n", item->name, item->phone);
fflush(fp); // 将内容刷新到磁盘上,之前内容是写到缓冲(内存)中
}
fclose(fp);
}
//@length:为buffer的实际长度(一条联系人的记录)
int parser_token(char *buffer, int length, char *name, char *phone)
{
if(buffer == NULL) return -1;
if(length < MIN_TOKEN_LENGTH) return -2;
int i = 0, j = 0, status = 0;
//获取到的数据内容形如name: wangbojing,telephone: 15889650380
//解析出姓名
//采用状态机的方法
for(i = 0; buffer[i] != ',';++i)
{
//遇到空格之后的状态就要改变,之后就为我们要的名字了
if(buffer[i] == ' ')
{
status = 1;
}
else if(status == 1)
{
name[j++] = buffer[i];
}
}
status = 0;
j = 0;
for(;i < length; ++i)
{
if(buffer[i] == ' ')
{
status = 1;
}
else if(status == 1)
{
phone[j++] = buffer[i];
}
}
INFO("file token : %s ---- %s\n", name, phone);
return 0;
}
//将filename文件中读取到的数据写入(加载到)到通讯录people中
//count:contacts对象的成员,改变通讯录记录条数
int load_file(struct person **ppeople, int *count,const char *filename)
{
FILE *fp = fopen(filename, "r");
if(fp == NULL)
return -1;
//fp内部指针不在文件尾就循环
while(!feof(fp))
{
char buffer[BUFFER_LENGTH] = {0};
//从fp所指的文件中读取长度BUFFER_LENGTH的数据到buffer中,遇到换行或结尾便停止
fgets(buffer, BUFFER_LENGTH, fp);
int length = strlen(buffer);
INFO("length : %d\n", length);
//name: wangbojing,telephone: 15889650380
char name[NAME_LENGTH] = {0};
char phone[PHONE_LENGTH] = {0};
//没有成功解析出内容,跳出本次循环
if(0 != parser_token(buffer, length, name, phone))
{
continue;
}
//插入通讯录(链表)
struct person *p = (struct person*)malloc(sizeof(struct person));
if(p == NULL) return -2;
//memcpy() 会复制 name 所指的内存内容的前 NAME_LENGTH 个字节到 p->name 所指的内存地址上。
memcpy(p->name, name, NAME_LENGTH);
memcpy(p->phone, phone, PHONE_LENGTH);
person_insert(ppeople, p);
(*count)++;
}
fclose(fp);
return 0;
}
//define interface end
//业务逻辑的实现
int insert_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
struct person *p = (struct person*)malloc(sizeof(struct person));
memset(p, 0x00, sizeof(struct person));
if(p == NULL)
return -2;
//name
INFO("Please input name: \n");
scanf("%s", p->name);
//phone
INFO("Please input phone: \n");
scanf("%s", p->phone);
//add people
//如果cts->people为NULL,我们要改变的是cts->people的值,所以必须传地址
//故person_insert这里第一个参数必须为二级指针
if(0 != person_insert(&cts->people, p))
{
free(p);
return -3;
}
cts->count++; //联系人数量更新
INFO("insert success!\n");
return 0;
}
int print_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
//cts->people
person_traverse(cts->people);
return 0;
}
int delete_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
//name
INFO("Please input name: \n");
char name[NAME_LENGTH] = {0};
scanf("%s",name);
//person
//先查找再删除
struct person *ps = person_search(cts->people, name);
if(ps == NULL)
{
INFO("Person don't exist!\n");
return -2;
}
//delete
//若删的是最后一个元素或者是第一个元素(第一个元素)
//则要改变people的值,所以这个参数要传地址
//故参数类型是二级指针struct person **
person_delete(&cts->people, ps);
free(ps);
INFO("Delete success!\n");
return 0;
}
int search_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
//name
INFO("Please input name: \n");
char name[NAME_LENGTH] = {0};
scanf("%s",name);
//person
struct person *ps = person_search(cts->people, name);
if(ps == NULL)
{
INFO("Person don't exist!\n");
return -2;
}
INFO("name:%s, phone: %s\n",ps->name, ps->phone);
return 0;
}
int save_entry(struct contacts * cts)
{
if(cts == NULL) return -1;
INFO("Please input save filename:\n");
char filename[NAME_LENGTH] = {0};
scanf("%s", filename);
save_file(cts->people, filename);
}
int load_entry(struct contacts * cts)
{
if(cts == NULL) return -1;
INFO("Please input load filename:\n");
char filename[NAME_LENGTH] = {0};
scanf("%s", filename);
load_file(&cts->people, &cts->count, filename);
}
void menu_info()
{
INFO("\n******************************************************\n");
INFO("*****1.Add Person\t\t2.Print Person *******\n");
INFO("*****3.Del Person\t\t4.Search Person ******\n");
INFO("*****5.Save Person\t\t6.Load Person ********\n");
INFO("*****Other Key for Exiting Program *******************\n");
INFO("\n******************************************************\n");
}
int main()
{
struct contacts *cts = (struct contacts*)malloc(sizeof(struct contacts));
if(cts == NULL) return -1;
//初始化结构体
memset(cts, 0x00, sizeof(struct contacts));
while(1)
{
menu_info();
int select = 0;
scanf("%d", &select);
switch(select)
{
//调用业务逻辑层的函数
case OPER_INSERT:
insert_entry(cts);
break;
case OPER_PRINT:
print_entry(cts);
break;
case OPER_DELETE:
delete_entry(cts);
break;
case OPER_SEARCH:
search_entry(cts);
break;
case OPER_SAVE:
save_entry(cts);
break;
case OPER_LOAD:
load_entry(cts);
break;
default:
goto exit;
}
}
exit:
free(cts);
return 0;
}
代码拆解分析
链表实现与数据结构定义
本程序使用双向链表来进行通讯录的存储 :
struct person
{
char name[NAME_LENGTH];
char phone[PHONE_LENGTH];
struct person *next;
struct person *prev; //设计的是双向链表,便于删除节点
};
通讯录的管理类,便于操作
struct contacts
{
struct person *people; // 指向通讯录链表
int count;
};
实现接口层的链表的头部插入和删除节点操作,注意宏定义是直接文本替换,注意加括号,防止优先级造成的问题。
#define LIST_INSERT(item, list) do{ \
item->prev = NULL; \
item->next = list; \
if((list) != NULL) (list)->prev = item; \
(list) = item; \
}while(0)
#define LIST_REMOVE(item ,list) do{ \
if(item->prev != NULL) item->prev->next = item->next; \
if(item->next != NULL) item->next->prev = item->prev; \
if(item == list) list = item->next; \
item->prev = item->next = NULL; \
}while(0) // 只执行一次
数据结构操作接口层的函数的实现
//define interface ,接口层会使用支持层的功能
//这里之所以ppeople要用二级指针是因为,当传入的若为NULL,即第一次插入节点的时候
//我们要修改其一级指针的值。
int person_insert(struct person **ppeople, struct person *ps)
{
if(ps == NULL) return -1;
LIST_INSERT(ps, *ppeople);
return 0;
}
//若删除的是最后一个元素,那么指向链表的指针(一级)要修改为NULL,所以要传二级指针
int person_delete(struct person **ppeople, struct person *ps)
{
if(ps == NULL) return -1;
LIST_REMOVE(ps, *ppeople);
return 0;
}
struct person * person_search(struct person *people, const char *name)
{
struct person *item = NULL;
for(item = people; item != NULL; item = item->next)
{
//strcmp相等为0
if(!strcmp(name, item->name))
break;
}
return item;
}
int person_traverse(struct person *people)
{
struct person *item = NULL;
for(item = people; item != NULL;item = item->next)
{
//这里INFO是宏定义
INFO("name: %s,phone: %s\n", item->name, item->phone);
}
return 0; // 成功返回0
}
业务逻辑函数的实现
将不同的操作用匿名枚举定义而不直接使用数字表示
enum {
OPER_INSERT = 1,
OPER_PRINT,
OPER_DELETE,
OPER_SEARCH,
OPER_SAVE,
OPER_LOAD
};
main函数给用户提供功能的显示和选择:
void menu_info()
{
INFO("\n******************************************************\n");
INFO("*****1.Add Person\t\t2.Print Person *******\n");
INFO("*****3.Del Person\t\t4.Search Person ******\n");
INFO("*****5.Save Person\t\t6.Load Person ********\n");
INFO("*****Other Key for Exiting Program *******************\n");
INFO("\n******************************************************\n");
}
int main()
{
struct contacts *cts = (struct contacts*)malloc(sizeof(struct contacts));
if(cts == NULL) return -1;
//初始化结构体
memset(cts, 0x00, sizeof(struct contacts));
while(1)
{
menu_info();
int select = 0;
scanf("%d", &select);
switch(select)
{
//调用业务逻辑层的函数
case OPER_INSERT:
insert_entry(cts);
break;
case OPER_PRINT:
print_entry(cts);
break;
case OPER_DELETE:
delete_entry(cts);
break;
case OPER_SEARCH:
search_entry(cts);
break;
case OPER_SAVE:
save_entry(cts);
break;
case OPER_LOAD:
load_entry(cts);
break;
default:
goto exit;
}
}
exit:
free(cts);
return 0;
}
业务逻辑层的函数实现
//业务逻辑的实现
int insert_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
struct person *p = (struct person*)malloc(sizeof(struct person));
memset(p, 0x00, sizeof(struct person));
if(p == NULL)
return -2;
//name
INFO("Please input name: \n");
scanf("%s", p->name);
//phone
INFO("Please input phone: \n");
scanf("%s", p->phone);
//add people
//如果cts->people为NULL,我们要改变的是cts->people的值,所以必须传地址
//故person_insert这里第一个参数必须为二级指针
if(0 != person_insert(&cts->people, p))
{
free(p);
return -3;
}
cts->count++; //联系人数量更新
INFO("insert success!\n");
return 0;
}
int print_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
//cts->people
person_traverse(cts->people);
return 0;
}
int delete_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
//name
INFO("Please input name: \n");
char name[NAME_LENGTH] = {0};
scanf("%s",name);
//person
//先查找再删除
struct person *ps = person_search(cts->people, name);
if(ps == NULL)
{
INFO("Person don't exist!\n");
return -2;
}
//delete
//若删的是最后一个元素或者是第一个元素(第一个元素)
//则要改变people的值,所以这个参数要传地址
//故参数类型是二级指针struct person **
person_delete(&cts->people, ps);
free(ps);
INFO("Delete success!\n");
return 0;
}
int search_entry(struct contacts *cts)
{
if(cts == NULL) return -1;
//name
INFO("Please input name: \n");
char name[NAME_LENGTH] = {0};
scanf("%s",name);
//person
struct person *ps = person_search(cts->people, name);
if(ps == NULL)
{
INFO("Person don't exist!\n");
return -2;
}
INFO("name:%s, phone: %s\n",ps->name, ps->phone);
return 0;
}
文件保存与加载的接口层实现
保存就是将链表上数据存储到磁盘文件上,加载就是从磁盘文件中读出联系人数据插入到链表中
文件保存接口层实现
//将通讯录(链表)people信息保存到文件filename中
int save_file(struct person *people, const char *filename)
{
FILE *fp = fopen(filename, "w");
if(fp == NULL) return -1;
struct person *item = NULL;
for(item = people; item != NULL; item = item->next)
{
fprintf(fp, "name: %s, phone: %s\n", item->name, item->phone);
fflush(fp); // 将内容刷新到磁盘上,之前内容只是写到缓冲(内存)中
}
fclose(fp);
}
文件加载接口层实现
此处,我们要从文件解析出名字以及电话号码,我们存在磁盘中的内容是形如:
name: wangbojing,telephone: 15889650380
根据这个格式,
-
我们可以通过逗号将名字和电话解析成两个部分即
name: wangbojing
和telephone: 15889650380
-
然后根据有限状态机,初始状态为0,遇到空格则状态变为1,之后遇到的非空格字符都是名字
电话号码解析也是同理。
//@length:为buffer的实际长度(一条联系人的记录)
int parser_token(char *buffer, int length, char *name, char *phone)
{
if(buffer == NULL) return -1;
if(length < MIN_TOKEN_LENGTH) return -2;
int i = 0, j = 0, status = 0;
//解析出姓名
//采用状态机的方法
for(i = 0; buffer[i] != ',';++i)
{
//遇到空格之后的状态就要改变,之后就为我们要的名字了
if(buffer[i] == ' ')
{
status = 1;
}
else if(status == 1)
{
name[j++] = buffer[i];
}
}
status = 0;
j = 0;
for(;i < length; ++i)
{
if(buffer[i] == ' ')
{
status = 1;
}
else if(status == 1)
{
phone[j++] = buffer[i];
}
}
INFO("file token : %s ---- %s\n", name, phone);
return 0;
}
加载文件的接口层实现:
//将filename文件中读取到的数据写入(加载到)到通讯录people中
//count:contacts对象的成员,改变通讯录记录条数
int load_file(struct person **ppeople, int *count,const char *filename)
{
FILE *fp = fopen(filename, "r");
if(fp == NULL)
return -1;
//fp内部指针不在文件尾就循环
while(!feof(fp))
{
char buffer[BUFFER_LENGTH] = {0};
//从fp所指的文件中读取长度BUFFER_LENGTH的数据到buffer中,遇到换行或结尾便停止
fgets(buffer, BUFFER_LENGTH, fp);
int length = strlen(buffer);
INFO("length : %d\n", length);
//name: wangbojing,telephone: 15889650380
char name[NAME_LENGTH] = {0};
char phone[PHONE_LENGTH] = {0};
//没有成功解析出内容,跳出本次循环
if(0 != parser_token(buffer, length, name, phone))
{
continue;
}
//插入通讯录(链表)
struct person *p = (struct person*)malloc(sizeof(struct person));
if(p == NULL) return -2;
//memcpy() 会复制 name 所指的内存内容的前 NAME_LENGTH 个字节到 p->name 所指的内存地址上。
memcpy(p->name, name, NAME_LENGTH);
memcpy(p->phone, phone, PHONE_LENGTH);
person_insert(ppeople, p);
(*count)++;
}
fclose(fp);
return 0;
}
文件保存与加载的业务逻辑层实现
int save_entry(struct contacts * cts)
{
if(cts == NULL) return -1;
INFO("Please input save filename:\n");
char filename[NAME_LENGTH] = {0};
scanf("%s", filename);
save_file(cts->people, filename);
}
int load_entry(struct contacts * cts)
{
if(cts == NULL) return -1;
INFO("Please input load filename:\n");
char filename[NAME_LENGTH] = {0};
scanf("%s", filename);
load_file(&cts->people, &cts->count, filename);
}
心得
这个程序函数通过三层设计方法让设计变得很清晰,同时利用宏定义增强可读性,对我来说加深了程序开发的一些经验,开阔了自己的视野。