代码中的软件工程--对menu项目的源码分析
简介:
本文章是基于孟宁老师在高级软件工程课堂上讲授的知识指导下完成的,主要是记录了采用VS Code来构建一个menu(https://github.com/mengning/menu)项目的实验过程,以及在阅读该项目源码的过程中对于软件工程的思想、方法和设计原则的思考。
参考资料:
实验环境:
操作系统:Windows 10
、
文档编辑器:VS Code 1.51.0
、
编译器和调试器:MinGW 8.1.0
(gcc
和 gdb
在Windows平台上的实现版本)。
目录:
一、VS Code上搭建C/C++的编译和调试环境。
二、对软件工程中设计艺术的思考。
三、实验小结。
一、VS Code上搭建C/C++的编译和调试环境
1.安装VS Code及C/C++开发插件
VS Code(Visual Studio Code)是由微软研发的一款免费、开源的跨平台文本(代码)编辑器。几乎完美的编辑器。
官网:https://code.visualstudio.com
文档:https://code.visualstudio.com/docs
源码:https://github.com/Microsoft/vscode
本次实验所用的项目文件由C语言编写而成,要完成项目的构建,需要先安装VS Code并在其上构建C/C++的编译和开发环境。具体操作如下:
先到VS Code的官网下载并安装VS Code,安装完成后打开软件,点击左侧管理拓展图标,在输入框中输入C/C++并选择插件进行安装。安装成功后如图所示:
2.下载编译器 MinGW-w64
,配置系统环境变量
上一步安装的开发插件中不包括编译器和调试器,所以我们需要MinGW-w64
作为运行C/C++项目的编译器和调试器。先到官网(https://sourceforge.net/projects/mingw-w64/files/)下载相应版本的安装包:
本实验选择MinGW-W64GCC-8.1.0
下的x86_64-posix-seh
进行下载,解压后得到的文件夹中找到一个名称为bin的文件夹:
然后将该目录加入系统环境变量中:
在命令行中输入gcc -v
,看到如下结果则表明安装成功。
3.在VS Code中配置编译路径
按快捷键F5
,对C程序进行编译,生成.vscode
文件夹,VS Code会在该文件夹下自动生成launch.json
文件,修改launch.json
文件如下:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "gcc.exe - 生成和调试活动文件", // 配置名称,将会在启动配置的下拉菜单中显示
"type": "cppdbg", // 配置类型,这里只能为cppdbg
"request": "launch", // 请求配置类型,可以为launch(启动)或attach(附加)
"program": "${workspaceFolder}/${fileBasenameNoExtension}.exe", // 将要进行调试的程序的路径
"args": ["-std=c++11"], // 程序调试时传递给程序的命令行参数,一般设为空即可
"stopAtEntry": false, // 设为true时程序将暂停在程序入口处,一般设置为false
"cwd": "${workspaceFolder}", // 调试程序时的工作目录,一般为${workspaceRoot}即代码所在目录 workspaceRoot已被弃用,现改为workspaceFolder
"environment": [],
"externalConsole": true, // 调试时是否显示控制台窗口,一般设置为true显示控制台
"MIMode": "gdb",
"miDebuggerPath": "D:/mingw-w64/mingw64/bin/gdb.exe", // miDebugger的路径,注意这里要与MinGw的路径对应
"preLaunchTask": "c/c++ g++.exe build active file", // 调试会话开始前执行的任务,一般为编译程序,c++为g++, c为gcc
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
当项目文件中存在.h类型的C语言头文件时,需要将.h文件所在的文件路径加入到编译路径中。此时需要在.vscode
文件夹下新建c_cpp_properties.json
文件,并将文件内容修改如下:
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceRoot}",
"D:/mingw-w64/mingw64/include/**",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/x86_64-w64-mingw32",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/backward",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include-fixed",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/../../../../x86_64-w64-mingw32/include",
"${workspaceFolder}/include" // 将自己编写的.h头文件放在该目录下
],
"defines": [
"_DEBUG",
"UNICODE",
"__GNUC__=6",
"__cdecl=__attribute__((__cdecl__))"
],
"intelliSenseMode": "msvc-x64",
"browse": {
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": "",
"path": [
"${workspaceRoot}",
"D:/mingw-w64/mingw64/include/**",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/x86_64-w64-mingw32",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include/c++/backward",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/include-fixed",
"D:/mingw-w64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/../../../../x86_64-w64-mingw32/include"
]
},
"cStandard": "c17",
"cppStandard": "c++20"
}
],
"version": 4
}
其中D:/mingw-w64/mingw64/
是MinGW-W64GCC-8.1.0
在我的电脑上的安装路径。除了需要配置这两个文件之外,还需要在.vscode
文件夹下新建tasks.json
文件,并将该文件的内容修改如下:
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "c/c++ g++.exe build active file",
"command": "D:/mingw-w64/mingw64/bin/g++.exe",
"args": [
"-g",
"${file}",
"-I", "D:/VSCode/se/src/lab7.1/include",
"D:/VSCode/se/src/lab7.1/linktable.c",
"D:/VSCode/se/src/lab7.1/menu.c",
"-o",
"${workspaceFolder}/${fileBasenameNoExtension}.exe",
"-std=c++11",
"-fexec-charset=GBK"//解决中文乱码
], // 编译命令参数
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": {
"owner": "cpp",
"fileLocation": [
"relative",
"${workspaceFolder}"
],
"pattern": {
"regexp": "^(.*):(/d+):(/d+):/s+(warning|error):/s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
},
"group": {
"kind": "build",
"isDefault": true
},
}
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
}
到此,环境的配置工作完成,menu项目的目录结构如下:
4.运行项目
运行项目中的test.c
文件,得到项目运行结果如下:
从上图可以看到test.c
文件运行成功,即利用VS Code搭建C/C++的编译和调试环境的实验顺利完成。
二、对软件工程中设计艺术的思考
1.对menu项目各个版本的源码进行分析
文件夹名称 | 子文件 | 功能描述 |
---|---|---|
lab1 |
hello.c/menu.c |
hello.c 文件的作用是测试开发环境是否搭建完成,menu.c 文件是该项目的原始结构(伪代码)。 |
lab2 |
menu.c |
对menu.c 进行完善,能够针对help和quit指令完成输出。为menu.c 添加了文件头部注释。 |
lab3.1 |
menu.c |
将指令通过链表组织起来,并为每个指令设计一个函数指针来处理指令对应的操作。 |
lab3.2 |
menu.c |
把对链表的查找操作封装到FindCmd 函数当中,对链表的显示操作封装到函数ShowAllCmd 当中。 |
lab3.3 |
linklist.c/linklist.h/menu.c |
将链表的声明部分和实现部分分开,分别放置到linklist.h 和linklist.c 文件当中;将业务逻辑封装到menu.c 文件中。 |
lab4 |
linktable.h/linketable.c/menu.c/test.c/testlinktable.c |
引入了动态创建命令链表包括增删改等功能,利用了信号量实现多线程环境下对链表的互斥操作,并进行了单元测试。 |
lab5.1 |
linktable.h/linktable.c/menu.c/testlinktable.c |
将链表的初始化操作从业务逻辑中分离,引入回调函数SearchCondition ,回调函数的调用函数SearchLinkTableNode 。 |
lab5.2 |
linktable.h/linktable.c/menu.c/Makefile |
引入回调函数对SearchLinkTableNode 进行改造,降低了数据结构与逻辑代码的耦合度,添加了makefile文件完成编译和连接程序。 |
lab7.1 |
linktable.h/linktable.c/menu.c/Makefile/menu.h/test.c |
将业务逻辑menu部分的功能分离出来成为一个独立模块,从而在test中实现对功能模块的调用。 |
lab7.2 |
lab7.1的子文件+readme.txt |
增加了 readme.txt 文件,记录了项目的功能及使用说明。 |
2.对menu项目中体现的软件工程思想的思考
模块化设计
(Modular design)所谓的模块化设计,简单地说就是将产品的某些要素组合在一起,构成一个具有特定功能的子系统,将这个子系统作为通用性的模块与其他产品要素进行多种组合,构成新的系统,产生多种不同功能或相同功能、不同性能的系列产品。模块化设计是绿色设计方法之一,它已经从理念转变为较成熟的设计方法。将绿色设计思想与模块化设计方法结合起来,可以同时满足产品的功能属性和环境属性,一方面可以缩短产品研发与制造周期,增加产品系列,提高产品质量,快速应对市场变化;另一方面,可以减少或消除对环境的不利影响,方便重用、升级、维修和产品废弃后的拆卸、回收和处理。
在menu项目中,一开始是将数据结构的定义、声明和业务逻辑写在同一个menu.c
文件中,从lab3.1
开始,数据结构的定义和数据结构上的操作逐渐分离,从而将数据结构模块化。从lab4
到lab5.2
则是将业务逻辑层与数据定义层逐渐分离的阶段,这使得业务逻辑与底层的数据定义实现解耦合。从lab7.1
到lab7.2
是将业务逻辑逐渐模块化的阶段,最终形成一个简洁易用的接口提供给高层进行调用。
项目逐渐模块化的过程降低了各个模块之间的耦合度,对需求不断发生变化的场景更加适用,从而达到了项目各模块之间“高内聚、低耦合”的设计目标。
可重用接口
接口是其实现和其客户程序之间的一份契约。实现必须提供接口中规定的功能,而客户程序必须根据接口中描述的隐式和显式的规则来使用这些功能。程序设计语言提供了一些隐式规则,来支配接口中声明的类型、函数和变量的使用。例如,C语言的类型检查规则可以捕获接口函数的参数的类型和数目方面的错误。一般来说,接口规格包含五个基本要素: 接口的目的; 接口使用前所需要满足的条件,一般称为前置条件或假定条件; 使用接口的双方遵守的协议规范; 接口使用之后的效果,一般称为后置条件; 接口所隐含的质量属性。
以menu项目中linktable.c
文件中的SearchLinkTableNode
函数为例,该函数的形参中的Condition参数是一个函数指针 int Condition(tLinkTableNode* pNode, void* args)
,这个参数可以利用函数回调为其他模块提供一个更加通用的接口,而且这个函数没有用到任何业务逻辑层的数据,只负责遍历整个链表,关于是否找到指定条件的节点的判断交由Condition所指向的回调函数来负责。当另外一个模块想要调用该接口时,只需要根据自己的需求去编写Condition函数即可,这样的设计,封装了各个模块的实现细节,使得代码的重用性得到了巨大的提升。
函数SearchLinkTableNode
的实现代码如下:
/*
* Search a LinkTableNode from LinkTable
* int Conditon(tLinkTableNode * pNode);
*/
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
if(pLinkTable == NULL || Conditon == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != NULL)
{
if(Conditon(pNode,args) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
业务层模块 menu.c
文件中对该接口的实现和调用如下:
/* 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);
}
可重入函数与线程安全
可重入函数:若一个程序或子程序可以“在任意时刻被中断,然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。也就是说,当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时,重新进入同一个子程序,仍然是安全的。
可重入函数应满足的条件:不能含有静态(全局)非常量数据、不能返回静态(全局)非常量数据的地址、只能处理由调用者提供的数据、不能依赖于单实例模式资源的锁、调用的函数也必需是可重入的。上述条件就是要求可重入函数使用的所有变量都保存在调用栈的当前函数帧(frame)上。因此,同一执行线程重入执行该函数时 加载了新的函数帧,与前一次执行该函数时使用的函数帧不冲突、不互相覆盖,从而保证了可重入执行安全。
线程安全:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
需要考虑线程安全的情况:访问共享的变量或资源, 会有并发风险, 比如对象的属性, 静态变量, 共享缓存, 数据库等;所有依赖时序的操作, 即使每一步操作都是线程安全的, 还是存在并发的问题;不同的数据之间存在绑定关系的时候。例如IP
与端口号. 只要修改了IP
就要修改端口号, 否则IP
也是无效的。 因此遇到这种操作的时候,要警醒原子的合并操作,要么全部修改成功, 要么全部修改失败。使用其他类的时候, 如果该类的注释声明了不是线程安全的,那么就不应该在多线程的场景中使用, 而应该考虑其对应的线程安全的类,或者对其做一定处理保证线程安全。
menu项目中线程安全的应用:menu项目中对于链表的修改、删除和添加操作由于访问共享的变量或资源, 会有并发风险。故在函数CreateLinkTable()
、DeleteLinkTable(tLinkTable *pLinkTable)
、AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
和DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
中利用线程互斥锁机制保证代码的线程安全。
/*
* LinkTable Type
*/
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
};
/*
* Create a LinkTable
*/
tLinkTable * CreateLinkTable()
{
tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable));
if(pLinkTable == NULL)
{
return NULL;
}
pLinkTable->pHead = NULL;
pLinkTable->pTail = NULL;
pLinkTable->SumOfNode = 0;
pthread_mutex_init(&(pLinkTable->mutex), NULL);
return pLinkTable;
}
/*
* Delete a LinkTable
*/
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;
}
/*
* Delete a LinkTableNode from LinkTable
*/
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;
}
注意:可重入函数的条件只是线程安全的必要条件,不是充分条件,多线程执行同一程序时可能在可重入函数之外的地方出现线程不安全的因素。
三、总结
通过一段时间的学习,终于完成了此次的实验,不仅学会了利用VS Code完成C/C++开发环境的搭建,也通过阅读menu项目的源码,懂得了许多软件工程中模块化设计、可重用接口、可重入函数及线程安全的知识,受益良多。