代码中的软件工程
这节课讲了代码中的软件工程,通过分析一个菜单子系统程序menu的开发过程,介绍了这个小程序由雏形发展到成型过程中所设计的软工的思想,这种代码的"生长"过程十分的heuristic,值得仔细推敲。
课程ppt的地址和小程序menu的链接在这儿:参考资料
1.C/C++编译调试环境配置
1.1安装Visual Studio Code
这里的ide选择使用vscode,vscode是一个轻量级且十分强大的代码编辑器,支持Windows, OS X和Linux。拥有丰富的插件生态系统,课通过安装插件来支持C++, C#, Python, PHP等其他语言。(vscode下载地址)
1.2安装C/C++扩展插件
打开VSCode——点击侧栏上的扩展插件图标——搜索C++——点击安装
1.3安装C/C++编译器和调试器
C/C++扩展插件中并不包含C++的编译器和调试器,需要我们自己下载。常用的编译器有:
- GCC on Linux -https://gcc.gnu.org
- Mingw-w64 is GCC for Windows 64 & 32 bits - http://mingw-w64.org
- Microsoft C++ compiler on Windows
- Clang for Xcode on macOS
我用的是windows64位操作系统,这里选择下载并安装MinGW-W64-install.exe。
安装过程中有几个选项需要说明:
- Version制定版本号,从4.9.1-8.x.0,按需选择,没有特殊要求就用最新版
- Architecture跟操作系统有关,64位系统选择x86_64,32位系统选择i686
- Threads设置线程标准可选posix或win32
- Exception设置异常处理系统,x86_64可选为seh和sjlj,i686为dwarf和sjlj
- Build revision构建版本号,选择最大即可
安装完成后,将目录下的/bin目录添加进环境变量后,打开cmd,执行gcc -v看看是否安装成功,如果看到版本号,说明安装成功了!下图是安装成功时的样子:

在你喜欢的地方建立一个C/C++项目,然后在这个目录下打开cmd,输入"code ."命令,打开vscode,这个目录就成了你的workspace,随便写一段代码,按ctrl+F5,会选择调试环境和编译器,调试选择C++(GDB/LLDB),编译器选择gcc.exe(针对C,g++.exe针对cpp),然后你的workspace下会生成一个.vscode的隐藏文件(对于linux),里面有两个文件:
- tasks.json(build instruction)
- launch.json(debugger settings)

其中tasks.json时告诉vscode怎么构建这个程序的,他会调用gcc/g++编译器创建一个可执行文件。如图中args的参数所示。

按ctrl+F5后终端就会显示运行结果。
2.代码中的软件工程
2.1模块化设计
模块化(Modularity)是在软件系统设计时保持系统内部各部分相对独立,以便每个部分可以被独立的进行设计和开发。这个做法背后的原理就是关注点的分离,这是在软件工程领域中最重要的原则,习惯上称之为模块化,翻译成中文的表述其实就是“分而治之”的方法。这就好比人处理复杂的问题时容易出错,而将复杂问题拆解为一个个简单的问题,就不那么容易出错了。
模块化在软件工程中的体现就是每一个软件模块只有一个单一的功能目标,并且相对独立与其它软件模块。这样做有几个好处:
- 软件模块容易理解并且容易开发
- 如果出现bug,更容易定位并修复
- 对整个软件系统做出更改和维护也更容易,因为模块之间相互独立
通常用耦合度和内聚度来衡量软件模块化的程度。
下面分析menu程序中的模块化思想。
/********************************************************************/ /* 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 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); /* * Search a LinkTableNode from LinkTable * int Conditon(tLinkTableNode * pNode,void * args); */ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); #endif /* _LINK_TABLE_H_ */
/**************************************************************************************************/ /* 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 "linktable.h" int Help(); int Quit(); #define CMD_MAX_LEN 128 #define DESC_LEN 1024 #define CMD_NUM 10 /* data struct and its operations */ typedef struct DataNode { tLinkTableNode * pNext; char* cmd; char* desc; int (*handler)(); } tDataNode; int SearchCondition(tLinkTableNode * pLinkTableNode, void * args) { char * cmd = (char*) args; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; } /* find a cmd in the linklist and return the datanode pointer */ tDataNode* FindCmd(tLinkTable * head, char * cmd) { return (tDataNode*)SearchLinkTableNode(head,SearchCondition,(void*)cmd); } /* 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); pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode); } return 0; } int InitMenuData(tLinkTable ** ppLinktable) { *ppLinktable = CreateLinkTable(); tDataNode* pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "help"; pNode->desc = "Menu List:"; pNode->handler = Help; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "version"; pNode->desc = "Menu Program V1.0"; pNode->handler = NULL; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "quit"; pNode->desc = "Quit from Menu Program V1.0"; pNode->handler = Quit; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); return 0; } /* menu program */ tLinkTable * head = NULL; main() { InitMenuData(&head); /* 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; } int Quit() { exit(0); }
上面两段代码中,第一段是这个小程序对底层用到的链表的数据结构以及其操作的定义,第二段是对这个小程序的业务逻辑的实现。可以看到,第一段中的函数只与链表有关,这就是模块化化设计的思想,将底层数据结构的定义与业务逻辑的实现分离,要求第一个模块只做一件事,就是让它做好链表数据结构和操作的设计,不设计菜单业务功能,从而达到易理解,易开发,易维护,易扩展的目的。
2.2可重用接口
2.2.1接口的理解
为什么要有接口这个东西?我们做好了初步的模块化设计,每个模块的功能内聚,模块与模块之间相对独立,但是,最终做成整个项目,是要把这些模块拿来用的,有点像搭积木一样搭起来,而为了让搭好的积木整体不散架,每个积木之间的接触部分就要相配合。
接口是互相联系的双方共同遵守的一种协议规范,在软件系统内部一般的接口方式时通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问做提供的服务。
在面向过程的编程中,接口定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对外开放的一组属性和方法的集合。一般来说,接口规格包含五个基本要素:
- 接口的目的
- 接口使用前所需要满足的条件,一般称为前置条件或假定条件
- 使用接口的双方遵守的协议规范
- 接口使用之后的效果,一般称为后置条件
- 接口所隐含的质量属性
2.2.2以一个例子分析接口
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
这是从链表中取出链表头节点的函数声明,以此为例我们先来理解接口规格包含的五个基本要素。
- 该接口的目标是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标;
- 该接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL;
- 使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;
- 使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;
- 该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索
int ShowAllCmd(tLinkTable * head) { tDataNode * pNode = (tDataNode*)GetLinkTableHead(head); while(pNode != NULL) { printf("%s - %s\n", pNode->cmd, pNode->desc); pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode); } return 0; }
这是调用了这个接口的地方。传入的参数head遵守了接口定义中的形参类型要求,是tKinktable*,而使用了这个接口后得到了链表的头节点,这里又用类型转换转成了别的类型的节点指针,隐含了多态的思想。并且由于输出类型可以被转换,大大增加了这个接口的可重用性。别的地方如果需要获得链表的头节点,也可以用这种方式来获取。
2.3线程安全
2.3.1线程的定义
线程是操作系统能够进行运算调度的最小单位。他包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。多核多线程的CPU可以让不同的进程运行在不同的CPU核的不同线程上,从而大大减少进程调度进程切换的资源消耗。
2.3.2 可重入函数
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
2.3.3 线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
2.3.4 函数的可重入性与线程安全之间的关系
可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;不可重入的函数一定不是线程安全的。
2.3.5 menu项目中的线程安全分析
这个项目中的可重入函数是对链表的各个操作,为了能保持各个线程运行这些操作时能保持线程安全,在链表中添加了互斥锁。
/* * LinkTable Type */ struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; };
如果要用到修改链表的操作,就得先上锁,防止别的线程访问时得到非预期的结果。
tLinkTableNode * p = pLinkTable->pHead; pthread_mutex_lock(&(pLinkTable->mutex));//上锁 pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; pthread_mutex_unlock(&(pLinkTable->mutex));//释放 free(p);
3.总结
模块化设计,可重入接口,线程安全是软件工程中很重要的思想,这三者相辅相成,缺一不可,没有可重入,模块化就没有用武之地,没有线程安全,可重入就可能带来灾难性的而后果,而没有模块化设计,可重入核线程安全就无从谈起。
浙公网安备 33010602011771号