见微知著:Menu小程序中的代码艺术
笔者之前一直使用Vistual Studio2019作为开发工具,而将VS Code一直简单的作为编辑器使用,并未深入了解VS Code。正好借着孟老师的软件工程课程深入学习一下如何将VS Code+gcc工具集作为开发环境。
本文下述内容大致分为几部分:
- 编译与环境配置
- Menu小程序的简要分析
- Menu小程序引发的一系列问题的思考
一、编译与环境配置
VsCode最基本的功能是作为编辑器,他并没有编译功能,因此我们下载好VS Code之后还需要下载编译器。
编译器我选择了MinGW,但是要注意,因为MinGW下载为外网环境,因为网络原因很有可能下载失败,建议直接下载离线文件。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
这里笔者提供离线文件的网盘地址,有需要的童鞋可以自行下载
链接:https://pan.baidu.com/s/1n3gZBlvuF2HIllhjzRM3qA 提取码:ch8u
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
如选择使用提供的mingw-get-setup.exe安装,选择好需要安装的工具,一直next即可
安装好了MinGW之后,我们需要配置环境变量才可使用gcc
环境变量配置流程很简单,向PATH下添加MinGW文件夹下的bin文件夹路径即可
安装好了MinGW并配置了环境变量之后,我们可以通过在cmd命令行运行
gcc -v
测试是否安装成功(安装成功如下图所示)

此时我们VS Code与编译器均已安装完成,基本环境搭建已完成,我们马上大功告成,要想在VS Code上成功的运行调试程序,我们还需进行配置文件的修改,此时我们以孟老师给出的src文件夹下的hello.c为例,我们要想运行hello.c,需要更改.vscode下的launch.json与c_cpp_properties.json文件
具体修改细节如下:
1 //c_cpp_properties.json文件修改处
2 "miDebuggerPath": "D:\\MinGW\\mingw64\\bin\\gdb.exe" 3 //launch.json文件修改gcc的路径 4 "compilerPath": "D:\\MinGW\\mingw64\\bin\\gcc.exe",
现在我们环境配置好,大功告成,通过gcc编译运行如下

如果我们嫌弃每次输入命令行编译很麻烦,这时候VS Code的一个优势便显示出来即丰富的插件,我们去安装如下插件

安装插件成功后,在用户界面右上角会提供一个运行按钮,点击运行按钮,VS Code便会自动执行脚本自动进行文件的编译与运行,如下图所示,至此,我们的环境配置大功告成。
![]()
二、Menu小程序的简要分析
src文件夹内,共有十个lab文件夹,一步步描述了Menu小程序的代码构建过程,下面我简要分析一下每一步修改的原因即体现的软件工程思想(注,为了方便分析与查看,下面每增加一个.h文件,便将该文件接口代码展示一下)
1.lab1
lab1下包含 hello.c 文件与 menu.c 文件,hello.c 文件用于测试环境是否搭建完成,menu.c 文件是本程序的框架代码(此处为伪代码)
2.lab2
lab2下移除了 hello.c 文件,与此同时完善了 menu.c 文件的伪代码,使其成为了可以运行的框架代码
3.lab3.1
lab3.1下将可能重复用到的代码段封装到函数内部(分别封装了 Help() 函数和Quit() 函数),同时引入了结构体链表,将指令,指令详细描述,指令对应函数存入一个结构体内,初步体现了可重用接口与模块化的思想
4.lab3.2
lab3.2将模块化与可重用接口的思想进一步体现,进一步将主函数内遍历结构体链表寻找cmd的操作封装到了函数 FindCmd() 内,同时设计了一个新函数 FindCmd() 来为用户提供使用帮助,这很大程度上提高了代码的可用性,用户可以通过 Help 获取相关帮助,提高了用户的体验感
5.lab3.3
lab3.3代码部分并没有改变,但是将结构体代码独立到一个文件 linklist.h 内,这极大程度的体现了模块化的思想,同时也降低了码农的阅读与修改代码的难度,代码的逻辑层次更加明显。同时还有一个细节,在 linklist.h 文件内进行函数接口的声明,而实现的细节存储到 linklist.c 文件内,这极大程度上提高了阅读效率。
linklish.h文件内接口如下所示:
typedef struct DataNode
{
char* cmd;
char* desc;
int (*handler)();
struct DataNode *next;
} tDataNode;
/* 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);
6.lab4
lab4改变较大,增加了 Menu 数据的初始化函数 InitMenuData() ,可以动态创建Menu,增加了数据的灵活性,同时新引入了新的结构体 LinkTable 并增加了线程安全的互斥量 mutex,LinkTable 比之前的 linklist 增加了更多的内容,可以很方便的增删查节点。LinkTable更进一层次的实现了模块化,内聚程度更进一步,将业务层与底层节点分离,与menu模块之间的耦合度降低。
linktable.h接口如下所示
/*
* 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_ */

本部分还引入了线程安全的概念,通过互斥量 mutex 为线程上锁与解锁,解决不同线程之间的内存安全问题。线程安全本质上其实是内存的安全!
例如,上述代码通过对mutex进行上锁与解锁,实现了 SumOfNode 变量内存的安全,当不同线程访问该变量时,若对该变量不加锁,便可能会发生读写冲突,造成内存存储数据的正确性得不到保障。
7.lab5.1
lab5.1引入了回调函数 callback,因笔者对callback函数之前了解不多,所以着重记录一下该方法
------------------------------------分割线-----------------------------------------
引用维基百科上对回调的定义:
In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback.
把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。如果代码立即被执行就称为同步回调,如果在之后晚点的某个时间再执行,则称之为异步回调。
回调机制提供了非常大的灵活性,这种灵活性是怎么实现的呢?乍看起来,回调似乎只是函数间的调用,但仔细一琢磨,可以发现两者之间的一个关键的不同:在回调中,我们利用某种方式,把回调函数像参数一样传入中间函数。可以这么理解,在传入一个回调函数之前,中间函数是不完整的。换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。
8.lab5.2
lab5.2 改进了函数SearchLinkTableNode()函数
//lab5.1 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)) //lab5.2 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
lab5.1内存在问题,底层代码反而需要依赖业务层代码回调参数,lab5.2内进行函数改造,将所需信息作为函数传入,降低耦合度为松散耦合。
lab5.2 同时还增加了Makefile文件,使用make命令来构建工程,会使原本复杂繁琐的工作简化许多,极大地提高了效率,代码如下,
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令.
# # Makefile for Menu Program # CC_PTHREAD_FLAGS = -lpthread //给右侧gcc等提供别名 CC_FLAGS = -c CC_OUTPUT_FLAGS = -o CC = gcc RM = rm RM_FLAGS = -f TARGET = menu OBJS = linktable.o menu.o all: $(OBJS) $(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) .c.o: $(CC) $(CC_FLAGS) $< //$<为自动变量 clean: $(RM) $(RM_FLAGS) $(OBJS) $(TARGET) *.bak
9.lab7.1
在lab7.1中,将分层设计思想进一步发挥,将 menu 层与 linktable 层一样,彻底从 main()函数内分离出来,实现了进一步的解耦合与模块化。
在本lab中,新增 MenuConfig() 函数与 ExecuteMenu() 函数,将 menu 层完全提取出来,并在menu.h文件内向外部提供相关 menu 功能的接口,menu.h内容如下所示
/* add cmd to menu */ int MenuConfig(char * cmd, char * desc, int (*handler)()); /* Menu Engine Execute */ int ExecuteMenu();
至此,彻底将 Menu 层,LinkTable 层彻底与main()函数分割开来,程序员只需在 main() 函数内调用相关接口便可实现Menu的初始化,修改与执行。程序员此时便彻底将底层封装了起来,现在只需要关注底层对外提供的接口便可实现对功能的修改。
10.lab7.2
lab7.2较lab7.1代码方面并无改动,而是增加了 readme.txt 文件,为用户提供了使用指南,提供了该项目的Build流程与操作指南。
至此,本项目搭建完毕,虽然 menu 项目并不算太大,但其中包含的软件工程的思想却体现的淋漓尽致,麻雀虽小,五脏俱全。模块化设计,可重用接口,线程安全,用户体验度等等在该项目中均有所体现,下面笔者就这些方面的谈一下自身的理解与总结。
三、Menu小程序引发的一系列问题的思考
1.模块化编程
模块化设计与可重用接口有着千丝万缕的联系,这都是模块化编程的内容,将程序的不同功能分割为独立的,可互换的模块。
目标是为了实现高内聚,低耦合的目标
将程序中不同功能分为不同的模块,一方面逻辑结构更加清晰,另一方面便于分工。不同的模块之间要协同合作才可以实现最终的功能,而不同模块之间是如何协同的呢,这就引入了接口的概念。接口:通常是说模块或组件留给外界使用的方式,称作接口,即你在使用这些模块或框架或者其他什么的时候,你所真正使用的东西就是API接口,另外接口在特定的编程语言中有特定的含义和语法要求。
我认为模块化设计主要目标是使得逻辑结构更加简单,将不同的功能模块分别实现,对外部仅仅提供API接口,隐藏了底部代码的实现细节,不仅保证了代码的安全性(防止不懂的用户随意修改而造成错误),而且也方便了程序员的调用,同时出现问题的时候,也方便了程序员进行Debug。还有一层次要的目标是为了方便分工,现如今软件的体量越来越大,许多工程不太可能靠一个人的力量完成,需要很多人协同合作,这样将不同的功能模块分给不同的程序员,其他程序员只要知道其他功能的接口便可调用该功能。
而可重用接口我认为主要目标是不要重复造轮子,真正的代码工程是追求效率的,很多功能既然已经实现了,若我们为了追求效率,当然希望直接调用他人已经实现的功能,这时,接口的作用便体现出来,当然了,若是我们为了学习,增强自己的代码能力,还是建议自己亲手实现以下相关功能。
menu小程序中,孟老师便是将模块化设计与可重用接口很好的体现出来,我们将该程序分为了三部分
-
linktable
-
menu
-
主函数
在 linktable.h 内提供了对 linktable 数据结构进行创建,增加,删除,查找操作的接口(函数声明在上面已经给出)
在 menu.h 内提供了对menu菜单进行配置与执行功能的接口
而程序员只需要在主函数内调用上面两个 .h 文件给出的接口,便可实现menu的所有功能。每个不同的模块内聚程度都很高,不同模块之间的耦合尽可能的降到了最低,不会因为单个模块出现问题,而导致其他模块出现问题。
2.线程安全
线程安全本质上就是内存的安全,
不可重入函数线程不安全,线程安全不一定是可重入函数,而可重入函数一定是线程安全的
确保线程安全最基本的操作时加锁,当对一个共享变量进行操作时,对其加锁,确保其他线程不会同时修改该变量,当操作完毕后进行解锁。
本例中,线程安全就是通过加锁的方式。
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;//确保线程安全的锁
};
//加锁与解锁示例
pthread_mutex_lock(&(pLinkTable->mutex));//对变量SumOfNode进行加锁
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));//对变量进行解锁
加锁属于互斥同步的一种, 互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
现在随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
我们在书写代码时,一定要注意线程安全问题,因为有些时候出现线程不安全的情况后,很长一段时间不会再出现类似情况,这种情况下程序员debug会十分痛苦... 因此要格外注意
这篇随笔到这就结束了,通过对孟老师 menu 小程序的分析,我也领悟了很多软件工程中的重要思想,在此也做了简要的总结与分析,若有错误,欢迎大家指出,最后感谢教授这门课的孟宁老师的教诲。

浙公网安备 33010602011771号