代码中的软件工程
调试环境的配置和编译
安装MinGW和添加环境变量
- Path环境变量,用于指定操作系统需要使用到的可执行程序的位置。c++的话要把MinGW(Minimalist GNUfor Windows包含GCC、GNU binutils 等工具,以及对等于 Windows SDK)的bin(binary包含用户的可执行文件)目录加入其中,从而使得os在任何目录下都可以使用其下的应用程序(gcc.exe, g++.exe)
- 不同于C++,平常使用的Java不仅要添加Path环境变量,还需要添加JAVA_HOME(编译、运行Java程序时,JRE会去该变量指定的路径中搜索所需的类(.class)文件,通过javac命令 可以将java源文件编译为class字节码文件)。 而Path则是运行字节码文件;由java虚拟机对字节码进行解释和运行

安装C++扩展

安装一键运行扩展

配置jason文件
-
lauch.json 用于指定调试语言环境,指定调试类型,执行编译好的文件
需要注意的是,fileDirname是vscode自动生成的不需要添加啥环境变量(包含应用程序将使用到的信息,保证操作系统在任何位置都能找到响应的变量)miDebuggerPath与自己电脑中的gcc对应,externalConsole打开,个人习惯。
{ "version": "0.2.0", "configurations": [ { "name": "gcc.exe - 生成和调试活动文件", "type": "cppdbg", "request": "launch", "program": "${fileDirname}\\${fileBasenameNoExtension}.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "C:\\MinGWold\\mingw64\\bin\\gdb.exe", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "C/C++: gcc.exe build active file" } ] } -
tasks.json 用于对文件的编译
{ "tasks": [ { "type": "cppbuild", "label": "C/C++: gcc.exe build active file", "command": "C:\\MinGWold\\mingw64\\bin\\gcc.exe", "args": [ "-g", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "options": { "cwd": "C:\\MinGWold\\mingw64\\bin" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build", "isDefault": true }, "detail": "Generated task by Debugger" } ], "version": "2.0.0" } -
c_cpp_properties.json, 用于配置编译器环境的,包括启动器代号、位数(这些是自定义的)、编译选项、启动设置、编译模式等。
{ "configurations": [ { "name": "Win32", "includePath": [ "${workspaceFolder}/**" ], "defines": [ "_DEBUG", "UNICODE", "_UNICODE" ], "compilerPath": "C:\\MinGWold\\mingw64\\bin\\gcc.exe", "cStandard": "gnu17", "cppStandard": "gnu++14", "intelliSenseMode": "gcc-x86" } ], "version": 4 }
运行结果

代码分析
目录结构

- 从目录结构中可以看出如下信息
- 很难看出该程序的逻辑,也不知道那部分为
- 具有模块化设计的思想, menu层主要负责和用户的交互,链表层模块存于linktable.c
- 点开各个文件浏览每个文件的功能
- linktable.c 为链码层的主要文件,定义了链表的节点LinkTable以及底层的创建删除添加搜素获取等功能
- linktable.h则是为menu.c提供了可以使用的接口
- menu.c定义了菜单的功能,回调函数、菜单的查询帮助等操作和用户数据节点DataNode
- menu.h 为test.c 提供了能够使用的函数
软件工程的思想
本文的分析方式通过软件设计的重要设计指导原则进行分析
注释
/********************************************************************/
/* Copyright (C) SSE-USTC, 2012-2013 */
/* */
/* FILE NAME : linktabe.c */
/* 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
* Provide right Callback interface by Mengning,2012/09/17
*
*/
通过上述的注释可以看到,文件头部罕有项目名称,所属的模块以及主要的功能等。通过孟老师上课讲的内容可以知道,良好的代码要么可以通过函数的名称定义等轻松的理解其逻辑功能,要么有详尽清晰地注释。作为刚开始学习写工程化代码的我,自然要将代码的注释写的清晰详尽。因此学习了如何为头部添加合适的代码
- 下载插件

- 启动头部注释快捷键 window:ctrl+alt+i,mac:ctrl+cmd+i, linux: ctrl+meta+i
- 添加函数注释: window:ctrl+alt+t,mac:ctrl+cmd+t,linux: ctrl+meta+t
注释结构如下图所示
/*
* @Author: your name
* @Date: 2020-11-03 21:29:15
* @LastEditTime: 2020-11-07 11:03:04
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \menu\test.c
*/\\
模块化
通过menu项目的目录结构便可以看出系统的模块化设计思想,具体为把整个menu项目划分为两个模块,其中业务层模块主要负责面向用户这部分位于menu文件中,而链码层模块主要负责存储底层的数据,以及提供相应的操作,这部分位于linktable之中。进而实现了功能内聚。
接口
参照接口的五个基本要素,对照老师上课讲的GetLinkTableHead接口,自行对SearchLinkTableNode进行分析
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
-
接口的目的:
设置SearchLinkTableNode的目的是为了在链码层中遍历寻找用户需要得到的信息 ,SearchLinkTableNode清晰地表明了该接口的目的
-
接口的前置条件
该接口的前置条件为(pLinkTable != NULL && Conditon != NULL)
-
接口双方的通信规范
该接口的通信规范是通过数据结构tLinkTableNode和tLinkTable以及回调函数int Conditon(tLinkTableNode * pNode, void * args)定义的
-
接口的后置条件
接口的后置条件为tLinkTableNode类型的指针作为返回值
-
接口所隐含的质量属性
接口的质量属性包括运行时的质量属性和开发时的质量属性,对于运行时质量属性,该接口需要在可以接受的延时范围内搜索到想要的结果,或者返回空值
对于开发时的质量属性,开接口设计了Callback,从而实现了链码层和用户层的解耦,大大提高了可重用性。具体细节如下,
int SearchCondition(tLinkTableNode * pLinkTableNode) { tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; } int SearchCondition(tLinkTableNode * pLinkTableNode, void * args) { char * cmd = (char*) args; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; }相较于版本1的回调函数实现方式,本文中的menu项目采用了第二种方式。可以看出,版本1中的cmd为全局变量,而版本二通过传入参数的方式简化了接口的前置条件,从而提高了该接口开发时的质量属性
信息隐藏
如图所示,相较于写法二,本项目所采用的写法一更能优秀,原因是将底部链码层的具体定义方式隐藏在了.c之中,并不暴露给用户,从而降低
typedef struct LinkTable tLinkTable; // 位于.h
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
}; //位于.c 写法一
typedef struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
}tLinkTable; //位于.h 写法二
增量开发
增量开发和迭代开发的关系
通俗的讲,迭代开发是以实现用户的需求为目的,然后再一次次的迭代使得项目不断改进。而增量开发是从一开始就定义好最终需要交付产品的方案,分模块一次次的实现。即类似于下图的区别
增量开发:

迭代开发

通过下载孟宁老师开发过程的源码,从不同版本的迭代中看到了增量开发的过程:

lab1 实现了对C++项目的简单测试,保证hello程序可以正常运行,lab2实现了menu菜单最基础的功能即help和quit命令,lab3.1加入了version并结构化了(tDataNode)代码,lab3.2抽离出了FindCmd和ShowAllCmd等接口,lab3.3 则加入了简易底层的linklist。lab4则实现了linktable的接口,lab5.1加入了Condition回调函数,降低了不同模块的耦合度。lab5.2则是简化了回调函数的前置条件,利用void * args取消了对全局变量的依赖。并隐藏了LinkTable。lab7.1在删除节点时保证了函数的可重入性和线程安全,lab7.2的便是最后交付形式的代码。从上述过程中充分展出除了增量开发的特点。
函数的可重入性与线程安全
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;
}
- 对修改加了锁,在此过程中不允许线程的重入, 保证两个或多个线程同时访问该函数时不会产生冲突,
- 在删除的过程中,pLinkTable->pHead = pLinkTable->pHead->pNext;删除操作一步完成,保证了链表不会因为删除过程中其它线程重入而导致链表的中断。从而保证了线程安全。
- 线程安全和可重入的关系:可重入指的是函数可以由多于一个任务并发使用,而不必担心数据错误。线程安全是指多个线程同时运行该段代码,其结果与单线程相同。因此可重入函数不一定是线程安全的,因为多个可重入的函数在多个线程中并发使用时,会存在共享全局变量和静态变量导致的冲突问题。

浙公网安备 33010602011771号