新手必做:一个简易通讯录带你入门C

43 阅读9分钟

概览

也许你是一位大学CS新生,也许你出于兴趣爱好刚学完C语法。

可是,你是否困惑?

  • 指针应该使用在哪?
  • 双重指针是不是老手的炫技?
  • 为什么需要malloc和free?
  • 宏是什么?
  • 数据结构有什么用?

一个基于C语言的简单通讯录,麻雀虽小,五脏俱全。它涉及了C语言语法中最让新手头疼的一些部分----指针、malloc等;涉及了一个可靠项目的大体结构搭建----业务、接口和底层;还涉及了与CS其他学科交叉的方面----数据结构、文件I/O。如果你刚学完C语法,不知道接下来应该干什么?那么这个项目将会是一个很好的开始----它足够简单:以至于你不会陷入复杂度的泥潭里失去对CS进一步学习的兴趣,它足够精炼:以至于你一定会学有所得,迫切地想要开展进一步的学习!


Github链接

基于Linux C的简易通讯录

通讯录的功能

  • 1.添加人员
  • 2.打印人员
  • 3.删除人员
  • 4.搜索人员
  • 5.保存通讯录 (高级)
  • 6.加载通讯录(高级)

项目实现架构

  • 业务层
  • 接口层
  • 支持层

IMG_0064.jpg

关键代码讲解


一、支持层

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退出
TODO:高级功能:加载通讯录、保存通讯录