孟老师通过演示Menu菜单,给我们讲解了软件工程应注意的许多问题,让我们收获很多。
老师重点讲解了代码风格、模块化和可重入函数与线程安全这几方面。
参考资料:https://github.com/mengning/menu
https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
1.首先要先安装C/C++扩展包,而这里我已经实现安装好了,下图所示便是vscode中对应的C/C++扩展包。
2.接下来,我们要安装和配置MinGW,经过查看网上提供的教程,这里我们选择下载离线安装包,这里我们选择安装X86_64-posix-seh。
下载下来的文件格式是7Z文件,用相应的解压软件解压后便可得到一个名为x86_64-8.1.0-release-posix-seh-rt_v6-rev0的文件夹。
3.接着便是将其添加至环境变量中。
我们要将mingw中的bin目录添加到系统的环境变量中,方法如下:
首先,找到“系统环境变量”的设置位置,在开始菜单的搜索栏搜索环境变量,点击编辑系统环境变量。
然后添加系统环境变量,依次:点击环境变量,找到系统变量中的path,点击编辑,然后点击新建,在使用浏览按钮选中到mingw下的bin文件夹即可。
这里添加进去的路径为E:\MinGW\x86_64-8.1.0-release-posix-seh-rt_v6-rev0\mingw64\bin,之后再依次点击确定,就完成了环境变量的设置。
在配置好环境变量后,可以在cmd命令行窗口验证是否配置成功,在cmd命令行窗口中输入gcc -v,如果配置成功,则会显示如下信息:
4.在vscode中创建配置文件
根据官方的文档,需要创建以下三个配置文件,以用于编译和运行cpp文件,分别是c_cpp_properties.json、launch.json和tasks.json。
这三个配置文件的内容分别如下图所示:
最后在配置完这三个文件后,便可以在vscode中打断点进行调试了。
二.代码风格分析
好的代码风格应该是简明、易读和无二义性。
我们写的代码首先要满足规范整洁,遵守常规语言规范,合理使用空格、空行、缩进、注释等;
其次我们要逻辑清晰,没有代码冗余、重复,让人清晰明了的命名规则。做到逻辑清晰不仅要求程序员的编程能力,更重要的是提高设计能力,选用合适的设计模式、软件架构风格可以有效改善代码的逻辑结构,会让代码简洁清晰;
最后要优雅,优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。
另外,注释在代码风格中也是很重要的一部分,一个好的注释能够让代码变得简明易懂,这对于今后项目的维护和管理用处很大,以下图片展示了一个好的注释风格应该是什么样的。
以上两张图展示了两个位于代码头部的注释,可见不管在什么项目中,都应该用心去写注释,以此来养成一个良好的代码习惯。
代码风格规范的总结:
1.缩进:4个空格;
2.行宽:< 100个字符;
3.代码行内要适当多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d));
4.在一个函数体内,逻揖上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行以示区隔;
5.在复杂的表达式中要用括号来清楚的表示逻辑优先级;
6.花括号:所有 ‘{’ 和 ‘}’ 应独占一行且成对对齐;
7.不要把多条语句和多个变量的定义放在同一行;
三.模块化思想
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。
这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns)。
模块化是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出,没错就是Dijkstra最短路径算法的作者。
关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。
关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
下面是没有使用模块化之前的代码,可以看出程序中有许多共同的操作散落在各处,这就对之后的维护带来了很大的麻烦,而且会导致程序非常冗长,给阅读造成很大的不便。
/**************************************************************************************************/ /* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015 */ /* */ /* FILE NAME : menu.c */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : menu */ /* MODULE NAME : menu */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : ANY */ /* DATE OF FIRST RELEASE : 2014/08/31 */ /* DESCRIPTION : This is a menu program */ /**************************************************************************************************/ /* * Revision log: * * Created by Mengning, 2014/08/31 * */ #include <stdio.h> #include <stdlib.h> #include <string.h> int Help(); int Quit(); #define CMD_MAX_LEN 128 #define DESC_LEN 1024 #define CMD_NUM 10 typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode; static tDataNode head[] = { {"help", "this is help cmd!", Help,&head[1]}, {"version", "menu program v1.0", NULL, &head[2]}, {"quit", "Quit from menu", Quit, NULL} }; 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; } int Quit() { exit(0); }
通过分析知道,上述的代码基本将所有操作全杂糅进了main函数中,通过阅读代码,可以提炼出一些程序中出现多次的共同操作,如:FindCmd( )和ShowAllCmd( )这类操作,将其提出来进行初步的模块化,编写为函数,这样做不仅精炼了代码,给阅读程序带来了便利,也便于今后的维护,因为此时只需要修改对应的函数即可,而不用将原来main函数中所有有关的操作全部找出来进行修改。修改后的代码如下:
/**************************************************************************************************/ /* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015 */ /* */ /* FILE NAME : menu.c */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : menu */ /* MODULE NAME : menu */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : ANY */ /* DATE OF FIRST RELEASE : 2014/08/31 */ /* DESCRIPTION : This is a menu program */ /**************************************************************************************************/ /* * Revision log: * * Created by Mengning, 2014/08/31 * */ #include <stdio.h> #include <stdlib.h> int Help(); #define CMD_MAX_LEN 128 #define DESC_LEN 1024 #define CMD_NUM 10 /* data struct and its operations */ typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode; 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; } /* menu program */ static tDataNode head[] = { {"help", "this is help cmd!", Help,&head[1]}, {"version", "menu program v1.0", NULL, NULL} }; int main() { /* cmd line begins */ while(1) { char cmd[CMD_MAX_LEN]; printf("Input a cmd number > "); scanf("%s", cmd); tDataNode *p = FindCmd(head, cmd); if( p == NULL) { printf("This is a wrong cmd!\n "); continue; } printf("%s - %s\n", p->cmd, p->desc); if(p->handler != NULL) { p->handler(); } } } int Help() { ShowAllCmd(head); return 0; }
这样修改后的代码基本有了模块化的结构了,阅读起来比之前的代码更清晰明了,但是还有点欠缺之处,因为上述的代码将所有的模块都放进了同一文件中,一旦遇到错误,修改起来还是会有些许不便之处。因此,我们继续将这些不同模块放进对应的不同源文件中,以便得到更好的效果,我们分出两个源文件分别是linklist.h与linklist.c文件,其代码分别如下:
/**************************************************************************************************/ /* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015 */ /* */ /* FILE NAME : linklist.h */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : menu */ /* MODULE NAME : linklist */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : ANY */ /* DATE OF FIRST RELEASE : 2014/09/10 */ /* DESCRIPTION : linklist for menu program */ /**************************************************************************************************/ /* * Revision log: * * Created by Mengning, 2014/09/10 * */ /* data struct and its operations */ typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode; /* find a cmd in the linklist and return the datanode pointer */ tDataNode* FindCmd(tDataNode * head, char * cmd); /* show all cmd in listlist */ int ShowAllCmd(tDataNode * head);
以上是linklist.h文件,接下来是linklist.c文件。
/**************************************************************************************************/ /* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015 */ /* */ /* FILE NAME : linklist.c */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : menu */ /* MODULE NAME : linklist */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : ANY */ /* DATE OF FIRST RELEASE : 2014/09/10 */ /* DESCRIPTION : linklist for menu program */ /**************************************************************************************************/ /* * Revision log: * * Created by Mengning, 2014/09/10 * */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "linklist.h" 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; }
学习笔记:
1.模块化可以理解为将数据结构和它的操作与菜单业务处理进行分离处理,尽管还是在同一个源代码文件中,但是已经在逻辑上做了切分,就可以认为有了初步的模块化。
2.进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。
3.使用本地化外部接口来提高代码的适应能力
4.先写伪代码的代码结构更好一些
5.KISS(Keep It Simple & Stupid)原则:
· 一行代码只做一件事
· 一个块代码只做一件事
· 一个函数只做一件事
· 一个软件模块只做一件事
四.可重用接口
尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,因此孟老师在这里引入了linktable模块,摒弃了之前的linklist,来让linktable专门来做与数据处理有关的业务,而不去涉及menu业务。
这种做法进一步加大了内聚程度,降低了耦合度,使得代码的模块化往更深一步发展,而且linktable也比之前的linklist要更为通用灵活,另外linktable中相应的接口在linktable.h这个头文件中可以看到。
以下是linktable.h(即linktable中的接口):
/********************************************************************/ /* Copyright (C) SSE-USTC, 2012-2013 */ /* */ /* FILE NAME : linktabe.h */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : LinkTable */ /* MODULE NAME : LinkTable */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : ANY */ /* DATE OF FIRST RELEASE : 2012/12/30 */ /* DESCRIPTION : interface of Link Table */ /********************************************************************/ /* * Revision log: * * Created by Mengning,2012/12/30 * */ #ifndef _LINK_TABLE_H_ #define _LINK_TABLE_H_ #include <pthread.h> #define SUCCESS 0 #define FAILURE (-1) /* * LinkTable Node Type */ typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }tLinkTable; /* * Create a LinkTable */ tLinkTable * CreateLinkTable(); /* * Delete a LinkTable */ int DeleteLinkTable(tLinkTable *pLinkTable); /* * Add a LinkTableNode to LinkTable */ int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); /* * Delete a LinkTableNode from LinkTable */ int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); #endif /* _LINK_TABLE_H_ */
学习笔记:
1.接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。
2.一般来说,接口规格包含五个基本要素:
· 接口的目的;
· 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
· 使用接口的双方遵守的协议规范;
· 接口使用之后的效果,一般称为后置条件;
· 接口所隐含的质量属性。
3.通用接口的定义方法:
· 参数化上下文
· 移除前置条件
· 简化后置条件
五.线程安全
如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
以下是会引起线程安全问题的代码:
int DeleteLinkTable(tLinkTable *pLinkTable) { if(pLinkTable == NULL) { return FAILURE; } while(pLinkTable->pHead != NULL) { tLinkTableNode * p = pLinkTable->pHead; pthread_mutex_lock(&(pLinkTable->mutex)); pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); free(p); } pLinkTable->pHead = NULL; pLinkTable->pTail = NULL; pLinkTable->SumOfNode = 0; pthread_mutex_destroy(&(pLinkTable->mutex)); free(pLinkTable); return SUCCESS; }
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; pLinkTable->pTail = pNode; } pLinkTable->SumOfNode += 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; }
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return FAILURE; } pthread_mutex_lock(&(pLinkTable->mutex)); if(pLinkTable->pHead == pNode) { pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; if(pLinkTable->SumOfNode == 0) { pLinkTable->pTail = NULL; } pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } tLinkTableNode * pTempNode = pLinkTable->pHead; while(pTempNode != NULL) { if(pTempNode->pNext == pNode) { pTempNode->pNext = pTempNode->pNext->pNext; pLinkTable->SumOfNode -= 1 ; if(pLinkTable->SumOfNode == 0) { pLinkTable->pTail = NULL; } pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } pTempNode = pTempNode->pNext; } pthread_mutex_unlock(&(pLinkTable->mutex)); return FAILURE; }
以上代码的功能是进行删除与添加节点,然而在这些操作执行时,假如有两个或两个以上的线程同时添加一个节点或者删除一个节点时,就会出现临界区问题,因此这里在操作时都会对mutex加锁,以便实现临界区的互斥访问。
五.总结:
在今后的学习与应用中,一定要重视代码风格、模块化设计、可重用接口和线程安全问题,打好基础并养成良好习惯,这样才能在今后的开发中写出优美的代码,不断提升自己的能力水平。
1.代码风格一定要简单易懂,没有歧义。
2.要灵活熟练应用模块化设计,提高开发的效率和质量,尽力提高模块内的内聚程度并降低模块间的耦合度。
3.应用可重用的接口,减少不必要的工作。
4.线程安全问题也是需要我们注意的,应在工作中多关注多线程下的临界区访问的问题。