孟宁老师在高级软件工程课上讲解了一个通用的菜单小程序,通过这个程序的逐步演化让我们学习软件工程的基本思想。
一. 编译和调试环境配置
1. 编译项目
项目源代码在https://github.com/mengning/menu
在项目目录下输入make,make程序会自动找到makefile并执行编译工作。

2. 调试环境配置
GDB在命令行下调试程序效率低下,而且不够直观,所以我们采用孟宁老师推荐的Visual Studio Code编辑器配合GDB调试程序。
在vscode中打开项目目录menu,在编辑器左侧选择Run选项,点击“Run and Debug”按钮,选择默认的“C++(GDB/LLDB)”,

接下来vscode会在项目目录下生成一个.vscode文件夹,里面有tasks.json和launch.json两个配置文件。launch.json中的preLaunchTask是调试之前执行的构建任务,在tasks.json中可以配置该任务。我们把task的名称改为menu,并且加入menu.c和linktable.c两个文件:


设置断点,切换到文件test.c,按下F5,就可以开始调试了。

二、项目分析
老师提供了menu项目的实验代码,这些代码展示了菜单程序如何从简单到复杂发展,源代码在https://gitee.com/mengning997/se/tree/master/src
1. 模块化的思想
模块化思想就是把一个复杂的系统分解成多个相对独立的模块,每个模块完成特定的功能。这么做有几个优点:
- 单个模块较为简单,更容易理解
- 修改方便,一个模块的变更只影响很少的几个软件模块
- 减少冗余代码,一个模块可以多次使用
- 更容易定位bug
- 减少命名冲突
项目lab2到lab4的演进就是一个软件从简单到复杂、从紧耦合到松散耦合的过程。
lab2接受一个输入,然后用if-else语句查找对应的指令。
lab3.1使用链表结构代替if-else分支语句,使查找过程变得更加简洁,同时把指令对应的操作分离出来,写成单独的函数。
在lab3.2中,可以看到只有menu.c一个文件,这个文件包含了对链表的定义、赋值以及查找等操作,
lab3.3把链表的一部分实现放到了单独的源文件中,但是链表的创建还是硬编码在menu.c文件中。
lab4真正实现了一个可以重复使用的链表,使得menu.c可以专注于处理输入的指令。
2. 可重用接口
lab4中链表节点定义如下:
/*
* LinkTable Node Type
*/
typedef struct LinkTableNode
{
struct LinkTableNode * pNext;
} tLinkTableNode;
和一般的链表节点不同,这里的节点中只有一个指向下一个节点的指针,而没有用来存放数据的变量。这是因为linktable是一个通用的链表接口,它适用于所有类型的数据,包括自定义的数据类型。为了使用它,我们需要在menu.c中定义包含数据部分的节点,实际使用时通过强制类型转换传入参数:
/* data struct and its operations */
typedef struct DataNode
{
tLinkTableNode * pNext;
char* cmd;
char* desc;
int (*handler)();
} tDataNode;
/* show all cmd in listlist */
int ShowAllCmd(tLinkTable * head)
{
tDataNode * pNode = (tDataNode*)GetLinkTableHead(head);
while(pNode != NULL)
{
printf("%s - %s\n", pNode->cmd, pNode->desc);
/* 强制类型转换,把 (tDataNode *) 转换成 (tLinkTableNode *) */
pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode);
}
return 0;
}
lab5.1增加的callback方式的函数,使用者只需要提供查找的方式,无需知道接口的内部结构,就能让链表搜索
3. 线程安全
线程类似于独立的进程,但是它们共享地址空间,从而能访问到相同的数据。在多线程的程序中,由于一段代码可能被中断,导致代码每次执行的结果都有可能不一样。可重入函数可以由多个任务并发使用,而不必担心数据错误。
在多线程情况下,链表的插入、删除操作存在读写操作,如果不加锁就可能出现运行异常,在实验中使用互斥量pthrad_mutex让linktable的插入、删除操作变成原子操作。
以链表的插入操作为例,pthread_mutex_lock和pthread_mutex_unlock包围了链表插入的核心操作:
/*
* Add a LinkTableNode to LinkTable
*/
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
if(pLinkTable == NULL || pNode == NULL)
{
return FAILURE;
}
pNode->pNext = NULL;
pthread_mutex_lock(&(pLinkTable->mutex));
if(pLinkTable->pHead == NULL)
{
pLinkTable->pHead = pNode;
}
if(pLinkTable->pTail == NULL)
{
pLinkTable->pTail = pNode;
}
else
{
pLinkTable->pTail->pNext = pNode; /* (1) */
pLinkTable->pTail = pNode; /* (2) */
}
pLinkTable->SumOfNode += 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));
return SUCCESS;
}
在不加锁的情况下,如果两个线程同时对 同一个链表进行插入,线程1执行到(1)处中断,等到线程2插入完成后再执行(2),就会造成pTail指针指向混乱。
浙公网安备 33010602011771号