从menu项目中分析软件工程思想和方法
1、前言
本文基于孟宁老师的课程内容及资料完成。
2、配置环境
1、打开VS Code,点击扩展栏搜索C++,安装该环境插件。 
2、以上插件并不包括C++的编译器和调试器,因此还需要单独下载。我们这里选择MinGW编译器。需要注意的是,官方文档要求安装路径不能存在空格和中文字符。
3、安装完成后,配置环境变量。详细步骤如下: 
4、查看该环境变量是否配置成功。打开cmd,键入gcc -v命令,输出如下信息则证明配置成功!
5、在VS Code中配置编译路径。
接下来配置编译器路径,按快捷键Ctrl+Shift+P调出命令面板,输入C/C++,选择“Edit Configurations(UI)”进入配置。
编译器路径选择D:\developer_Tools\minGW\mingw64\bin\g++.exe。
此处需要说明g++和gcc的主要区别:
1、对于.c文件gcc当做c语言处理,g++当做c++处理;对于.cpp文件gcc和g++均当做c++处理;
2、g++编译时实际上是调用gcc进行编译;
3、gcc不能自动链接库文件,一般用g++来链接库文件,非要用gcc的话,一般使用gcc -lstdc++命令;
4、extern “c”对于gcc和g++没有区别;
5、实际使用时只需安装gcc和g++中的一个就行了,如果使用gcc,编译直接用gcc就行了,链接要加上-lstdc++参数;如果使用g++,编译时实际还是调用gcc,链接直接使用g++即可。
配置完成后,此时在侧边栏生成了.vscode文件夹,并且里面有一个c_cpp_properties.json的配置文件。

此时编译运行Hello.c,并查看输出结果。
6、在VS Code中配置构建任务。
创建一个task.json,来告诉VS Code如何构建(编译)程序。该任务将调用g++编译器基于源代码创建可执行文件。


此时可以看到,在侧边栏生成了tasks.json文件。图中注释解释了其参数涵义:
7、在VS Code中配置调试设置。
此配置会在.vscode文件夹下生成一个launch.json文件,用来配置调试的相关信息。可以手动添加配置,也可点击右下方按钮添加。


配置完成后,按F5进行调试,测试结果如下:

至此,VS Code的编译和调试环境配置已完成!
3、使用Makefile来构建工程
然而,由于VS Code只有基本的编辑功能,并不能像其它IDE那样可以自动生成构建工程文件,当存在多个源文件需要编译时,需要在task.json文件中进行指定,操作过于繁琐!
因此我们引入自动构建工具Makefile。Makefile的使用十分灵活,可以手写也可以自动生成。在本次课程的项目中,Makefile文件已经准备好,我们只需要修改task.json配置文件就可以很方便的构建工程了。我们首先替换task.json如下图所示(设置两个任务分别为build和clean,参数分别为“all”和“clean”,它们会自动到Makefile文件下寻找这两个命令):
1 { 2 "tasks": [ 3 { 4 "label": "build", 5 "command": "mingw32-make", 6 "args": ["all"], 7 "type": "shell" 8 }, 9 { 10 "label": "clean", 11 "command": "mingw32-make", 12 "args": ["clean"], 13 "type": "shell" 14 } 15 ], 16 "version": "2.0.0" 17 }
Makefile文件如下图所示:例如 “all”命令,将会执行gcc -o test linktable.o menu.o test.o命令。

然后,我们需要告诉启动配置文件launch.json如何编译工程(设置启动任务"preLaunchTask":"build",它会自动到tasks.json中寻找标签为build的任务进行执行)如图所示:

最后,点击调试按钮,程序成功执行起来。(可以修改"program"属性来执行不同的test源文件)

4、模块化设计
所谓的模块化设计,简单地说就是将产品的某些要素组合在一起,构成一个具有特定功能的子系统,将这个子系统作为通用性的模块与其他产品要素进行多种组合,构成新的系统,产生多种不同功能或相同功能、不同性能的系列产品。
模块化是在软件系统设计时保持系统内部各部分的相互独立,也就是降低整个系统的耦合度,以便各个系统可以被独立的设计开发。其核心思想在于“分而治之”,将一个复杂的问题分解成若干独立的小问题,然后逐一解决。通过模块化设计,用高内聚低耦合的原则来实现系统,可以极大得提高软件开发效率,降低维护成本。
下面我们通过具体的项目和代码来说明如何进行模块化设计:
以下代码使用了链表数据结构,但并没有抽取出来,而是和业务代码糅杂在一起。这种写法可读性很差,后期维护难度大。
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int Help();
5 int Quit();
6
7 #define CMD_MAX_LEN 128
8 #define DESC_LEN 1024
9 #define CMD_NUM 10
10
11 typedef struct DataNode
12 {
13 char* cmd;
14 char* desc;
15 int (*handler)();
16 struct DataNode *next;
17 } tDataNode;
18
19 static tDataNode head[] =
20 {
21 {"help", "this is help cmd!", Help,&head[1]},
22 {"version", "menu program v1.0", NULL, &head[2]},
23 {"quit", "Quit from menu", Quit, NULL}
24 };
25
26 int main()
27 {
28 /* cmd line begins */
29 while(1)
30 {
31 char cmd[CMD_MAX_LEN];
32 printf("Input a cmd number > ");
33 scanf("%s", cmd);
34 tDataNode *p = head;
35 while(p != NULL)
36 {
37 if(strcmp(p->cmd, cmd) == 0)
38 {
39 printf("%s - %s\n", p->cmd, p->desc);
40 if(p->handler != NULL)
41 {
42 p->handler();
43 }
44 break;
45 }
46 p = p->next;
47 }
48 if(p == NULL)
49 {
50 printf("This is a wrong cmd!\n ");
51 }
52 }
53 }
54
55 int Help()
56 {
57 printf("Menu List:\n");
58 tDataNode *p = head;
59 while(p != NULL)
60 {
61 printf("%s - %s\n", p->cmd, p->desc);
62 p = p->next;
63 }
64 return 0;
65 }
66
67 int Quit()
68 {
69 exit(0);
70 }
我们使用模块化思想对以上代码进行重构,即将数据结构及其相关操作和具体的菜单业务处理进行分离,实现代码解耦。
#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;
}
以上代码虽然实现了逻辑上的切分,但是在一个源文件中,不符合模块化的原则,且不利于代码的重用。因此,我们应该将对数据结构操作部分的代码整理到单独的源文件中,并进行接口的设计,从而达到更高程度的解耦,以便于模块化之间进行相互调用。
5、可重用接口设计
尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。
在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。
在本项目中,我们对操作链表数据结构的代码单独设计接口:
1 #ifndef _LINK_TABLE_H_ 2 #define _LINK_TABLE_H_ 3 4 #include <pthread.h> 5 6 #define SUCCESS 0 7 #define FAILURE (-1) 8 9 /* 10 * LinkTable Node Type 11 */ 12 typedef struct LinkTableNode 13 { 14 struct LinkTableNode * pNext; 15 }tLinkTableNode; 16 17 /* 18 * LinkTable Type 19 */ 20 typedef struct LinkTable 21 { 22 tLinkTableNode *pHead; 23 tLinkTableNode *pTail; 24 int SumOfNode; 25 pthread_mutex_t mutex; 26 }tLinkTable; 27 28 /* 29 * Create a LinkTable 30 */ 31 tLinkTable * CreateLinkTable(); 32 /* 33 * Delete a LinkTable 34 */ 35 int DeleteLinkTable(tLinkTable *pLinkTable); 36 /* 37 * Add a LinkTableNode to LinkTable 38 */ 39 int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); 40 /* 41 * Delete a LinkTableNode from LinkTable 42 */ 43 int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); 44 /* 45 * Search a LinkTableNode from LinkTable 46 * int Conditon(tLinkTableNode * pNode); 47 */ 48 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)); 49 /* 50 * get LinkTableHead 51 */ 52 tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); 53 /* 54 * get next LinkTableNode 55 */ 56 tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); 57 58 #endif /* _LINK_TABLE_H_ */
在使用通用的Linktable模块之后menu程序业务代码变得复杂了一些,使用起来比较繁琐,是因为我们的接口定义的还不够好,我们对接口设计继续改进。
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 }
以上代码引入了回调函数 callback,所谓回调函数,就是将一个函数作为参数传递给另外一个函数,而这个函数可能会在某个时刻被回调执行,这样可以增加程序的灵活性和动态性。以上代码利用了callback函数,有效地提高了接口的通用性。
6、可重入函数与线程安全
可重入函数,简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。可重入函数,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode可重入,可以允许有多个该函数的副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。
了解了可重入函数的基本概念,我们需要注意在编写可重入函数时,如果涉及到公共资源的使用,必须对其加以保护,即进行同步操作来避免产生线程安全的问题。
1 int DeleteLinkTable(tLinkTable *pLinkTable) 2 { 3 if(pLinkTable == NULL) 4 { 5 return FAILURE; 6 } 7 while(pLinkTable->pHead != NULL) 8 { 9 tLinkTableNode * p = pLinkTable->pHead; 10 pthread_mutex_lock(&(pLinkTable->mutex)); //加锁 11 pLinkTable->pHead = pLinkTable->pHead->pNext; 12 pLinkTable->SumOfNode -= 1 ; 13 pthread_mutex_unlock(&(pLinkTable->mutex)); //解锁 14 free(p); 15 } 16 pLinkTable->pHead = NULL; 17 pLinkTable->pTail = NULL; 18 pLinkTable->SumOfNode = 0; 19 pthread_mutex_destroy(&(pLinkTable->mutex)); 20 free(pLinkTable); 21 return SUCCESS; 22 } 23 24 /* 25 * Add a LinkTableNode to LinkTable 26 */ 27 int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) 28 { 29 if(pLinkTable == NULL || pNode == NULL) 30 { 31 return FAILURE; 32 } 33 pNode->pNext = NULL; 34 pthread_mutex_lock(&(pLinkTable->mutex)); //加锁 35 if(pLinkTable->pHead == NULL) 36 { 37 pLinkTable->pHead = pNode; 38 } 39 if(pLinkTable->pTail == NULL) 40 { 41 pLinkTable->pTail = pNode; 42 } 43 else 44 { 45 pLinkTable->pTail->pNext = pNode; 46 pLinkTable->pTail = pNode; 47 } 48 pLinkTable->SumOfNode += 1 ; 49 pthread_mutex_unlock(&(pLinkTable->mutex)); //解锁 50 return SUCCESS; 51 }
以上代码(lab7.1)演示了处理线程安全的方法。即在对公共对象进行添加删除操作时(本例中为链表),使用同步锁来确保线程安全,而对于读操作,则无需考虑同步问题。
7、小结
本篇随笔主要参考了孟宁老师的课程资料及博客,从模块化设计、可重用接口设计以及线程安全等方面进行了代码的演示及笔记总结。

浙公网安备 33010602011771号