代码中的软件工程分析
代码中的软件工程分析----结合menu项目分析
一:环境准备
1 安装c/c++语言工具插件
2 安装,此处我们选择 MinGW-w64
MinGW 的全称是:Minimalist GNU on Windows 。它实际上是将经典的开源 C语言 编译器 GCC 移植到了 Windows 平台下,并且包含了 Win32API ,因此可以将源代码编译为可在 Windows 中运行的可执行程序。而且还可以使用一些 Windows 不具备的,Linux平台下的开发工具。一句话来概括:MinGW 就是 GCC 的 Windows 版本 。
以上是 MinGW 的介绍,MinGW-w64 与 MinGW 的区别在于 MinGW 只能编译生成32位可执行程序,而 MinGW-w64 则可以编译生成 64位 或 32位 可执行程序。
正因为如此,MinGW 现已被 MinGW-w64 所取代,且 MinGW 也早已停止了更新,内置的 GCC 停滞在了 4.8.1 版本。
因为在墙内,选择官网的下载器安装,老是出现下载不正确的问题,我们选择下载免安装版,解压后,将bin目录添加到环境变量中,打开控制台查看版本,此处是8.1.0最新版

3 程序从HelloWorld 开始
创建一个HelloWorld文件,我们放在d盘下,注意路径不要有中文,可能会出现一些奇怪的问题。在vs中打开,按快捷键Ctrl+Shift+P调出命令面板,输入C/C++,选择“Edit Configurations(UI)”进入配置,方便起见我们使用g++。配置完成后在test文件夹下生成了.vscode文件夹,并且里面有一个c_cpp_properties.json的配置文件。

1.g++会自动链接C++标准库,比如algorith,string,vector等。
2.gcc会根据文件后缀(.c,.cpp)自动识别是C文件还是C++文件,g++均认为是C++文件。
3.gcc编译C文件少很多宏定义,gcc编译C++会多一些宏定义,均属于the GNU Compiler Collection,gcc是鼻祖,后来才有了g++。
新建控制终端,进行编译和运行,成功输出hello,world

4 在vscode中启动调试模式
点击运行,启动调试运行,这里我们选用g++ ,会给我们生成两个json文件,launch.json和tasks.json文件,
在HelloWorld.c文件中打上断点,我们运行结果如下



到这里,我们成功完成编译环境和debug环境的设置。
这两个json文件的相关变量是vscode特有的,但是和其他系统环境变量写法相似的变量,在网上找到相关说明如下:
| 变量名 | 含义 |
|---|---|
| ${workspaceRoot} | 当前打开的文件夹的绝对路径+文件夹的名字 |
| ${workspaceRootFolderName} | 当前打开的文件夹的名字 |
| ${file} | 当前打开正在编辑的文件名,包括绝对路径,文件名,文件后缀名 |
| ${relativeFile} | 从当前打开的文件夹到当前打开的文件的路径,如当前打开的是test文件夹,当前的打开的是main.c,并有test/first/second/main.c 那么此变量代表的是 first / second / main.c |
| ${fileBasename} | 当前打开的文件名+后缀名,不包括路径 |
| ${fileBasenameNoExtension} | 当前打开的文件的文件名,不包括路径和后缀名 |
| ${fileDirname} | 当前打开的文件所在的绝对路径,不包括文件名 |
| ${fileExtname} | 当前打开的文件的后缀名 |
| ${cwd} | 任务开始运行时的当前工作目录 |
| ${lineNumber} | 前打开的文件,光标所在的行数 |
5 使用makefile 构建工程
在menu项目中我们只需要修改task.json和launch.json配置文件就可以很方便的构建工程了,最后进行测试结果如下:

二 代码中的软件工程方法
1 模块化软件设计
模块化: 把程序划分成若干个模块, 每个模块完成一个子功能, 把这些模块集总起来组成一个整体,可以完成指定的功能,满足问题的功能.
- 将系统划分成模块
- 决定每个模块的功能
- 决定模块的调用关系
- 决定模块的界面, 即模块间传递的数据
模块化是在软件系统设计时保持系统内部各部分的相互独立,也就是降低整个系统的耦合度,以便各个系统可以被独立的设计开发。其核心思想在于“分而治之”,将一个复杂的问题分解成若干独立的小问题,然后逐一解决。通过模块化设计,用高内聚低耦合的原则来实现系统,可以极大得提高软件开发效率,降低维护成本。模块化是好的软件设计的一个基本准则,可减小钥匙所需要的总工作。
分解 分解 高层模块 =======> 分解复杂问题 ========> 较小问题
模块化程度是软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度,耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。
这里引用课上ppt图片,对模块化做一个形象的展示

在一下代码中,DataNode储存了命令名、命令描述、函数和下一结点的指针,将数据结构和它的操作从菜单业务中分离出来,独立定义。
/* 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);
typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode;
上面对数据进行定义,而将业务逻辑抽离出来,形成一个模块,让各个模块之间的功能,和边界界限更加鲜明,尽最大可能做到解耦,我们将业务逻辑编写在下面。
#include <stdio.h> #include <stdlib.h> #include "linklist.h" int Help(); #define CMD_MAX_LEN 128 #define DESC_LEN 1024 #define CMD_NUM 10 /* menu program */ static tDataNode head[] = { {"help", "this is help cmd!", Help,&head[1]}, {"version", "menu program v1.0", NULL, NULL} }; 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; }
将复杂的问题抽离出去,调用其接口。每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。也使得后期维护变得简单一些,整个软件系统更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。代码中体现了松散耦合的思想和最求,遵循了KISS原则。
KISS原则:
一行代码只做一件事
一块代码只做一件事
一个函数只做一件事
一个软件模块只做一件事
同时使用了本地化外部接口来提高代码的适用能力:

2 可重用接口设计
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
接口规格包含五个基本要素:
接口的目的;
接口使用前所需要满足的条件,一般称为前置条件或假定条件;
使用接口的双方遵守的协议规范;
接口使用之后的效果,一般称为后置条件;
接口所隐含的质量属性。
如上四个关键因素需要按照顺序依次评估。
* * 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 int SearchCondition(tLinkTableNode * pLinkTableNode) 2 { 3 tDataNode * pNode = (tDataNode *)pLinkTableNode; 4 if(strcmp(pNode->cmd, cmd) == 0) 5 { 6 return SUCCESS; 7 } 8 return FAILURE; 9 } tNode* pTempNode = (tNode*)SearchLinkTableNode(pLinkTable,SearchCondition);
3 可重入函数与线程安全
基本定义
线程安全:简单来说线程安全就是多个线程并发同一段代码时,不会出现不同的结果,我们就可以说该线程是安全的;
线程不安全:说完了线程安全,线程不安全的问题就很好解释,如果多线程并发执行时会产生不同的结果,则该线程就是不安全的。
线程安全产生的原因:
大多是因为对全局变量和静态变量的操作
常见的线程不安全的函数
(1)不保护共享变量的函数
(2)函数状态随着被调用,状态发生变化的函数
(3)返回指向静态变量指针的函数
(4)调用线程不安全函数的函数
常见的线程安全的情况
(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; } /* * 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; pLinkTable->pTail = pNode; } pLinkTable->SumOfNode += 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); //解锁 return SUCCESS; }
4 总结
软件设计的方法与原则:
第一个原则:单一原则
第二个原则:里式替换原则
第三个原则:依赖倒置原则
第四个原则:接口隔离原则
第五个原则:迪米特法则
第六个原则:组合聚合原则
第七个原则:开闭原则

浙公网安备 33010602011771号