概览
也许你是一位大学CS新生,也许你出于兴趣爱好刚学完C语法。
可是,你是否困惑?
- 指针应该使用在哪?
- 双重指针是不是老手的炫技?
- 为什么需要malloc和free?
- 宏是什么?
- 数据结构有什么用?
一个基于C语言的简单通讯录,麻雀虽小,五脏俱全。它涉及了C语言语法中最让新手头疼的一些部分----指针、malloc等;涉及了一个可靠项目的大体结构搭建----业务、接口和底层;还涉及了与CS其他学科交叉的方面----数据结构、文件I/O。如果你刚学完C语法,不知道接下来应该干什么?那么这个项目将会是一个很好的开始----它足够简单:以至于你不会陷入复杂度的泥潭里失去对CS进一步学习的兴趣,它足够精炼:以至于你一定会学有所得,迫切地想要开展进一步的学习!
Github链接
通讯录的功能
- 1.添加人员
- 2.打印人员
- 3.删除人员
- 4.搜索人员
- 5.保存通讯录 (高级)
- 6.加载通讯录(高级)
项目实现架构
- 业务层
- 接口层
- 支持层
关键代码讲解
一、支持层
Linus Torvalds 有一些非常著名的关于数据结构的评论:
"我见过太多人因为‘应该使用更复杂的数据结构’而把代码搞砸。实际上,99%的情况下,你只需要一个简单的链表或哈希表就够了。"
"糟糕的程序员关心代码,优秀的程序员关心数据结构和它们之间的关系。"
不只是我们传奇的Linus,也许你会好奇,为什么CS的世界里,大家都在谈论着数据结构?----因为数据结构代表着一个程序的底层逻辑,我们的实现接口和业务逻辑,都与这个底层逻辑挂钩。它影响着项目的效率、复杂度、可维护性、甚至趣味性。其实严格来说,不了解数据结构,就意味着不懂任何代码。
支持层实现了最底层的数据结构,在通讯录项目中,使用双向链表来实现有如下好处:
- 可动态拓展的大小
- 高效率(O(1))的插入和删除操作
- 最自然的遍历:一个for从头到尾
而通讯录本身的数据结构其实只是这个双向链表的头节点。
struct person {
char name[NAME_LENGTH];
char phone[PHONE_LENGTH];
struct person* next;
struct person* prev;
};
struct contacts {
struct person* people;
int count;
};
二、接口层
本项目中,接口层函数的功能和实现都十分简单易懂,但是值得注意的是,对于上述双向链表的底层数据结构的具体操作,是使用宏的方式来实现的。
之所以使用宏来实现对底层数据结构的操作,有几方面的考虑,首先,宏可以避免函数调用的时候产生的开销,提升性能。其次,由于也不涉及复杂的逻辑操作,只是移动节点的指针之类的简单操作,所以使用宏所带来的调试复杂度可以忽略。
//用宏来实现对链表的insert和remove功能
#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 (list == item) list = item->next; \
item->prev = item->next = NULL; \
} while(0)
定义好这一系列宏之后,接口层就得以调用他们来实现接口函数了。
//接口层逻辑实现
int person_insert(struct person **ppeople, struct person *ps) {
if (ps == NULL) return -1;
if (ppeople == NULL) return -2;
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) {
if (!strcmp(name, item->name))
break;
}
return item;
}
int person_traversal(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;
}
//结束接口层逻辑
语法难点讲解:双重指针的使用
双重指针,顾名思义即为指针的指针,我们知道:在C语言中普通指针的基本功能是用于改变所指变量的值。可是当我们需要改变指针本身所指向的内容时,双重指针才能满足我们的需求。
假如在上述代码的person_insert()函数当中,我们使用people的普通指针(*people)而不是双重指针(**ppeople)作为参数传递,我们会遇到一个情况:
people -> next == NULL
也就是说,我们的头节点people并没有指向任何实在的下一节点,而是NULL值。所以实现插入操作,无法直接在一个实在的下一节点的基础之上简单地对next和prev作更改。这时候,我们必须对让people指向的内容从NULL变成我们想要的实在的下一节点,也就是说,需要改变指针本身所指向的内容。
三、业务层
业务层的目标是隐藏对底层数据结构的访问的复杂度,让使用者得以聚焦于使用添加人员、删除人员等基本功能,或者,保存、加载通讯录等高级功能。通过调用接口层的函数来实现业务逻辑。
基本功能:添加、删除、打印、遍历
int insert_entry(struct contacts *cts) {
if (cts == NULL) return -1;
struct person *p = (struct person*)malloc(sizeof(struct person));
if (p == NULL) return -2;
INFO("请输入姓名:\n");
scanf("%s", p->name);
INFO("请输入号码:\n");
scanf("%s", p->phone);
if (0 != person_insert(&cts->people, p)) {
free(p);
return -3;
}
cts->count++;
return 0;
}
int print_entry(struct contacts *cts) {
if (cts == NULL) return -1;
person_traversal(cts->people);
return 0;
}
int delete_entry(struct contacts *cts) {
if (cts == NULL) return -1;
INFO("请输入姓名:\n");
char name[NAME_LENGTH] = {0};
scanf("%s", name);
struct person *ps = person_search(cts->people, name);
if (ps == NULL) {
INFO("不存在此人\n");
return -2;
}
person_delete(&cts->people, ps);
free(ps);
return 0;
}
int search_entry(struct contacts *cts) {
if (cts == NULL) return -1;
INFO("请输入名字:\n");
char name[NAME_LENGTH] = {0};
scanf("%s", name);
struct person *ps = person_search(cts->people, name);
if (ps == NULL) {
INFO("不存在此人\n");
return -2;
}
INFO("姓名:%s,号码:%s\n", ps->name, ps->phone);
return 0;
}
语法难点讲解:malloc() & free()函数
之前我们提到了双向链表的好处,第一个就是动态调整大小。具体的代码实现如上面第4行代码所示。我们在插入一个新节点的时候,需要先使用malloc分配所需的空间,而且这样做的时候,是程序运行的时候,即运行时。这样就起到了动态调节链表大小的功效。
struct person *p = (struct person*)malloc(sizeof(struct person));
当我们调用malloc时,需要告诉它,我们需要sizeof(struct person)这么多的空间,也就是一个新节点的空间,然后它会返回一个指针,指向我们的新节点,以便我们进行操作。
这里,只有当异常发生的时候才需要调用free释放空间,第14行,只有当添加节点的操作失败了,返回非0值的时候才会执行。
整合业务功能
其实这部分就很简单了,最直截了当的方式就是在main函数中用一些条件语句来整合各个业务层逻辑。我这里使用的是switch+枚举的方式实现,其实多if-else也可以。
enum {
OPEN_INSERT = 1,
OPEN_PRINT,
OPEN_DELETE,
OPEN_SEARCH,
OPEN_SAVE,
OPEN_LOAD
};
以上是在开头就定义好的枚举变量,这么做主要是为了可读性。
int main() {
struct contacts *cts = malloc(sizeof(struct contacts));
if (cts== NULL) return -1;
memset(cts, 0, sizeof(struct contacts));
while (1) {
menu_info();
int select = 0;
scanf("%d", &select);
switch (select) {
case OPEN_INSERT:
insert_entry(cts); //添加
break;
case OPEN_PRINT:
print_entry(cts); //打印
break;
case OPEN_DELETE:
delete_entry(cts); //删除
break;
case OPEN_SEARCH:
search_entry(cts); //遍历
break;
}
}
}
实机演示
添加与打印:添加Muggle和Mage
neil@192 PhoneBook % ./a.out
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
1
请输入姓名:
Muggle
请输入号码:
123
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
1
请输入姓名:
Mage
请输入号码:
321
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
2
name: Mage, phone: 321
name: Muggle, phone: 123
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
删除与打印:删除Muggle
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
3
请输入姓名:
Muggle
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
2
name: Mage, phone: 321
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
搜索人员:我们先添加Alice和Bob,然后尝试搜索Mage
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
2
name: Bob, phone: 654
name: Alice, phone: 456
name: Mage, phone: 321
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出
4
请输入名字:
Mage
姓名:Mage,号码:321
1.添加人员 2.打印人员 3.删除人员 4.搜索人员
功能测试中:5.保存通讯录 6.加载通讯录
Ctrl-C退出