代码中的软件工程
前言
本文主要使用了VScode + gcc工具,编译调试了孟宁老师的案例menu。
1、环境配置和编译
1.1 下载编译器:MinGW-w64 - for 32 and 64 bit Windows 往下稍微翻一下,选最新版本中的x86_64-posix-seh。
1.2 安装并配置环境:将压缩包解压缩,找到bin文件夹的完整路径,比如在本机上的路径是 C:\mingw64\bin ,将路径添加到环境变量的path中。具体操作如下:(Windows 10)此电脑-右击-高级系统设置-环境变量,点开后,path中,添加上路径 C:\mingw64\bin 。


1.3 验证编译器是否安装成功:win + R, 显示cmd回车:在终端命令行输入gcc,提示 no input files,说明安装成功。输入gcc -v 显示当前安装版本。


1.4 下载vscode,注意vscode只是一个纯文本编辑器(editor),不是IDE(集成开发环境),不含编译器(compiler)和许多其它功能,所以编译器要自己装好。在此不作赘述。
1.5 安装vscode的拓展:
- C/C++:又名 cpptools,提供Debug和Format功能
- Code Runner:右键即可编译运行单文件,很方便;但无法Debug

1.6 配置文件
首先新建文件夹(即工作区)test,打开vscode,打开此文件夹,在文件夹内写了一个测试代码hello.cpp,如下:

VSCode可以一键生成基础配置文件:
-
新建一个
.vscode文件夹,存放配置文件,新建三个(只需三个)配置文件,即c_cpp_properties.json、launch.json、tasks.json。注意:将此文件夹放在常用文件夹顶层,就不需要重复配置了。如果不先创建此文件夹,将无法完美生成配置文件,这可能是插件的一个逻辑错误。我相信很多人都知道配置引导这件事,但是很多时候自动生成的配置文件无法正常工作,原因就在这里。 -
打开你创建的 C 文件,
F5运行。不出意外的话会弹出配置引导面板,在弹出面板中依次选择以下选项:- C++ (GDB/LLDB)
- gcc.exe - 生成和调试活动文件
稍等片刻,会自动在.vscode文件夹下生成launch.json和tasks.json。这样生成的两个配置文件是相互匹配好的,可以直接用于基本的运行和调试。 如果是 C++文件,选择g++.exe编译器即可。
在本项目中,需要生成的配置文件如下:
c_cpp_properties.json
1 1 {
2 2 "configurations": [
3 3 {
4 4 "name": "Win32",
5 5 "includePath": [
6 6 "${workspaceFolder}/**"
7 7 ],
8 8 "defines": [
9 9 "_DEBUG",
10 10 "UNICODE",
11 11 "_UNICODE"
12 12 ],
13 13 "windowsSdkVersion": "10.0.17763.0",
14 14 "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/bin/Hostx64/x64/cl.exe",
15 15 "cStandard": "c11",
16 16 "cppStandard": "c++17",
17 17 "intelliSenseMode": "msvc-x64"
18 18 }
19 19 ],
20 20 "version": 4
21 21 }
launch.json
1 {
2 "version": "0.2.0",
3 "configurations": [{
4 "name":"g++.exe - 生成和调试活动文件", // 配置名称,将会在启动配置的下拉菜单中显示
5 "type": "cppdbg", // 配置类型,cppdbg对应cpptools提供的调试功能;可以认为此处只能是cppdbg
6 "request": "launch", // 请求配置类型,可以为launch(启动)或attach(附加)
7 "program": "${fileDirname}/${fileBasenameNoExtension}.exe", // 将要进行调试的程序的路径
8 "args": [], // 程序调试时传递给程序的命令行参数,一般设为空即可
9 "stopAtEntry": false, // 设为true时程序将暂停在程序入口处,相当于在main上打断点
10 "cwd": "${workspaceFolder}", // 调试程序时的工作目录,此为工作区文件夹;改成${fileDirname}可变为文件所在目录
11 "environment": [], // 环境变量
12 "externalConsole": true, // 使用单独的cmd窗口,与其它IDE一致;为false时使用内置终端
13 "internalConsoleOptions": "neverOpen", // 如果不设为neverOpen,调试时会跳到“调试控制台”选项卡,你应该不需要对gdb手动输命令吧?
14 "MIMode": "gdb", // 指定连接的调试器,可以为gdb或lldb。但我没试过lldb
15 "miDebuggerPath": "gdb.exe", // 调试器路径,Windows下后缀不能省略,Linux下则不要
16 "setupCommands": [
17 { // 模板自带,好像可以更好地显示STL容器的内容,具体作用自行Google
18 "description": "Enable pretty-printing for gdb",
19 "text": "-enable-pretty-printing",
20 "ignoreFailures": false
21 }
22 ],
23 "preLaunchTask": "Compile" // 调试会话开始前执行的任务,一般为编译程序。与tasks.json的label相对应
24 }]
25 }
tasks.json
1 {
2 "tasks": [
3 {
4 "type": "shell",
5 "label": "C/C++: g++.exe build active file",
6 "command": "C:\\mingw64\\bin\\g++.exe",
7 "args": [
8 "-g",
9 "${file}",
10 "-o",
11 "${fileDirname}\\${fileBasenameNoExtension}.exe"
12 ],
13 "options": {
14 "cwd": "${workspaceFolder}"
15 },
16 "problemMatcher": [
17 "$gcc"
18 ],
19 "group": {
20 "kind": "build",
21 "isDefault": true
22 }
23 }
24 ],
25 "version": "2.0.0"
26 }
1.7 运行文件,点击F5,成功调试。
2.模块化设计
模块是指整个系统中一些相对对独立的程序单元,每个程序单元完成和实现一个相对独立的软件功能。通俗点就是一些独立的程序段。 模块设计也叫详细设计,是系统设计阶段后续的一个软件开发阶段。在系统设计阶段要把整个应用问题分解成一个个独立的功能部分--叫做程序模块。 每个程序模块要有自己的名称、标识符、接口等外部特征。
模块化设计和编程有以下几点好处:
1、便于设计与编程
2、便于分工合作 对于较大的项目这种方式的好处最为明显。
3、便于调试 可先对每个小模块进行单独调试,发现问题快,解决也快。在每个小模块都检测通过后,整体调试出现问题只要检查各模块相关联的部分(如参数地址、参数类型)是否统一即可。
4、便于移植 其他项目中如果有相同的应用,可以直接复制。如果只是近似应用,也只需做局部小改动即可。可省去大量的不必要的重复工作。5、便于改进
模块独立程度的2个定性度量标准
•内聚:模块内各元素的相关程度
•耦合:模块间的交互程度
整个项目的开发过程可以清楚的看到模块化思想:

3.接口的可重用性
一般来说,接口规格包含五个基本要素:
1.接口的目的;
2.接口使用前所需要满足的条件,一般称为前置条件或假定条件;
3.使用接口的双方遵守的协议规范;
4.接口使用之后的效果,一般称为后置条件;
可重用接口,则是其他程序员在重用这个接口时应该不需要了解这个接口内部代码的组织方式,只需要了解调用接口和生成的目标文件,就可以方便的将接口集成到自己的软件中。
如下述代码就用到了很多接口:
1 typedef struct LinkTableNode
2 {
3 struct LinkTableNode * pNext;
4 }tLinkTableNode;
5
6 typedef struct LinkTable tLinkTable;
7
8 tLinkTable * CreateLinkTable();
9
10 int DeleteLinkTable(tLinkTable *pLinkTable);
11
12 int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
13
14 int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
15
16 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
17
18 tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
19
20 tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
以第18行分析:
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
该接口是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标;接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL;接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;
4. 线程安全问题
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
常见的解决线程安全的方法有:
- 同步方法&静态同步方法
- 使用Lock锁
- 同步代码块
例如下述代码:
1 /*
2 * Delete a LinkTable
3 */
4 int DeleteLinkTable(tLinkTable *pLinkTable)
5 {
6 if(pLinkTable == NULL)
7 {
8 return FAILURE;
9 }
10 while(pLinkTable->pHead != NULL)
11 {
12 tLinkTableNode * p = pLinkTable->pHead;
13 pthread_mutex_lock(&(pLinkTable->mutex));
14 pLinkTable->pHead = pLinkTable->pHead->pNext;
15 pLinkTable->SumOfNode -= 1 ;
16 pthread_mutex_unlock(&(pLinkTable->mutex));
17 free(p);
18 }
19 pLinkTable->pHead = NULL;
20 pLinkTable->pTail = NULL;
21 pLinkTable->SumOfNode = 0;
22 pthread_mutex_destroy(&(pLinkTable->mutex));
23 free(pLinkTable);
24 return SUCCESS;
25 }
在第13行通过给信号量加锁,16行解锁,实现了多进程在删除链表时的互斥操作,保证了线程的安全。
总结:
通过孟老师的menu案例,我学习了软件工程设计中的模块化设计思想、接口的可重用性和多线程安全等知识。
reference:
https://gitee.com/mengning997/se/blob/master/README.md#%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B

浙公网安备 33010602011771号