代码中的软件工程
编译和调试环境配置
安装C/C++ 插件
安装C/C++编译器
C/C++插件只是让vs code具有处理C++的操作,但是具体的编译和debug还是需要专用的编译器来完成。编译器的种类有很多,这里采用孟宁老师推荐的Mingw-w64/GCC。
- 在浏览器输入https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe/download下载文件。
- 双击exe文件进行安装。
- 查看是否安装成功
- 运行代码
代码理解与分析
我们结合孟宁老师的menu项目的每一次迭代来理解软件开发的各种思想,沿着如下的路径:讲一讲每一次迭代解决了什么问题,还存在什么问题,然后下一次迭代进行解决。
初始(lab2)
首先看看lab2:
int main()
{
char cmd[128];
while(1)
{
scanf("%s", cmd);
if(strcmp(cmd, "help") == 0)
{
printf("This is help cmd!\n");
}
else if(strcmp(cmd, "quit") == 0)
{
exit(0);
}
else
{
printf("Wrong cmd!\n");
}
}
}
lab2通过 if... else...
语句来遍历所有的可能的命令。如果我们此刻有一千种cmd命令,就需要一千行if... else...
语句。很显然,这不利于后期的维护。
解耦(lab3.1、lab3.2)
为了解决lab2的if地狱,我们要对项目进行解耦。以孟宁老师lab3为例,结合代码来展示如何进行解耦:
lab3.1
为了不使用太多的if...else...判断
, 我们换一种思路:把各种命令放到一个链表中,每次找到对应的节点,然后执行节点对应的操作(hanlder函数):
//节点如下,包含 cmd名称和具体的操作函数handler
typedef struct DataNode
{
char* cmd;
char* desc;
int (*handler)();
struct DataNode *next;
} tDataNode;
然后我们看一下lab3.1中的main()
和help()
int main()
{
/* cmd line begins */
while(1)
{
char cmd[CMD_MAX_LEN];
printf("Input a cmd number > ");
scanf("%s", cmd);
tDataNode *p = head;
while(p != NULL)
{
if(strcmp(p->cmd, cmd) == 0)
{
printf("%s - %s\n", p->cmd, p->desc);
if(p->handler != NULL)
{
p->handler();
}
break;
}
p = p->next;
}
if(p == NULL)
{
printf("This is a wrong cmd!\n ");
}
}
}
int Help()
{
printf("Menu List:\n");
tDataNode *p = head;
while(p != NULL)
{
printf("%s - %s\n", p->cmd, p->desc);
p = p->next;
}
return 0;
}
两个函数直接在内部对链表进行操作时,看起来很合理,但是随着我们项目逐渐发展壮大后,会有很多问题,比如:
- 代码显得十分乱,每次阅读代码都要仔细查看和理解对链表做了什么操作。
- 每当我们需要新增一个对链表进行查询的功能,那我们就要在新的函数中将链表查询的操作再写一遍,这样会很费力。
- 当cmd命令数量足够多时,我们觉得链表查找效率太低了,想用树的结果来存储,那么我们除了要改变
DataNode
的结构,我们还要改变每一个使用DataNode的函数的代码。一旦后期项目变得很大,这个任务是非常重的。
lab3.2
为了解决lab3.1的耦合太深问题,我们对其进行解耦,将对链表的操作抽离出去:
tDataNode* FindCmd(tDataNode * head, char * cmd)
{
if(head == NULL || cmd == NULL)
{
return NULL;
}
tDataNode *p = head;
while(p != NULL)
{
if(!strcmp(p->cmd, cmd))
{
return p;
}
p = p->next;
}
return NULL;
}
int ShowAllCmd(tDataNode * head)
{
printf("Menu List:\n");
tDataNode *p = head;
while(p != NULL)
{
printf("%s - %s\n", p->cmd, p->desc);
p = p->next;
}
return 0;
}
当我们要使用的时候,直接调用函数即可:
int main(){
....
tDataNode *p = FindCmd(head, cmd);
...
}
int Help()
{
ShowAllCmd(head);
return 0;
}
这样就可以解决上面的三个问题了:
- 对链表操作的函数无需关注细节,直接调用就好,代码易于理解(前提是良好的语义化命名函数)
- 如果新增查找链表的功能,直接调用函数即可,无需重写代码。
- 如果我们想把链表改成树结构,改变链表和函数即可,基本不需改变调用者。
现在的代码仍然有不足之处,比如:
- 如何工期紧张,我们想要两个人分工开发。但是现在必须完成链表的全部代码,才能写menu的代码,这样即使分工也和一个人的时间差不多(甚至一个人写还减少了交流成本)。
- 如果其他项目或者这个项目的其他部分想要使用链表的内容,那么只能
Ctrl c
和Ctrl v
了,无法很好地实现代码复用。
为了解决这一问题,我们需要进行可重用设计。
可重用(lab3.3)
为了实现代码复用和分工,我们把链表操作分离到一个单独的文件,并且用头文件(.h)作为外部接口来告诉别的文件如何使用链表操作。
//linklist.h
tDataNode* FindCmd(tDataNode * head, char * cmd);
/* show all cmd in listlist */
int ShowAllCmd(tDataNode * head);
在menu中使用时,导入头文件即可使用:
//menu.c
#include "linklist.h"
int main(){
...
tDataNode *p = FindCmd(head, cmd);
...
}
int Help()
{
ShowAllCmd(head);
return 0;
}
这样一来,如果两个开发者要分工开发,只需要一个商量好 头文件(.h),然后就可以各自按照头文件的规范进行开发,互不干扰,可以大大缩短开发的时间。
在单线程条件下,现在的代码已经堪称“完美”。但是一旦出现在多线程情况下,就会出现多种问题,比如线程a在链表中加入了一个新命令,但是数据还没从缓存写入内存,这时候线程b也在链表加入了一个新命令,线程a的修改就被覆盖了。
线程安全
为了实现线程安全,孟宁老师在menu中采用了加 互斥锁 的方式。在加锁期间,其他线程不能访问修改该数据。
pthread_mutex_t的用法如下:
//初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex)
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
在 linktable.c
中,当初始化 tLinkTable
时,初始化锁:
tLinkTable * CreateLinkTable()
{
......初始化链表
//初始化异步锁
pthread_mutex_init(&(pLinkTable->mutex), NULL);
return pLinkTable;
}
当对链表进行增删改时加锁:
//删除链表
int DeleteLinkTable(tLinkTable *pLinkTable)
{
....
//当删除每一个结点时,加锁,改变链表后,解锁
pthread_mutex_lock(&(pLinkTable->mutex));
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));
free(p);
}
......
//销毁锁
pthread_mutex_destroy(&(pLinkTable->mutex));
free(pLinkTable);
return SUCCESS;
}
//删除链表节点
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
......
//加锁
pthread_mutex_lock(&(pLinkTable->mutex));
.....
//寻找到节点,删除节点,然后解锁
pthread_mutex_unlock(&(pLinkTable->mutex));
....
//找不到节点,解锁,返回失败
pthread_mutex_unlock(&(pLinkTable->mutex));
return FAILURE;
}
//增加链表节点
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
.....
//加锁
pthread_mutex_unlock(&(pLinkTable->mutex));
......
//找到对应的位置,插入节点
pLinkTable->pTail = pNode
pLinkTable->SumOfNode += 1 ;
//解锁
pthread_mutex_unlock(&(pLinkTable->mutex));
return SUCCESS;
}
孟宁老师的menu代码中还有很多值得我们学习,比如如何注释,如何进行测试,makefile等等。但是由于篇幅有限,本篇博客就不再详细解读这部分内容了。