代码中的软件工程
0.前言
本博客以VS Code + GCC工具集为主要环境,编译调试了孟宁老师的课程项目案例,学习到了软件工程方法、规范和软件工程思想。接下来,我将对自己学到的知识进行总结分析。
1.编译和环境配置
1.1环境配置
1.下载VS Code
2.下载C/C++插件
3.下载C/C++编译器和调试器
4.下载Mingw-w64/GCC
5.配置tasks.json,launch.json,setting.json文件
在配置这3个文件时,我遇到了一些问题,通过参考网上的资料并对其进行修改之后,可使得配置正确。
setting.json:
{ "files.associations": { "tidl_alg_int.h": "c", "limits": "c" } }
tasks.json:
{ // 有关 tasks.json 格式的参考文档:https://go.microsoft.com/fwlink/?LinkId=733558 。 "version": "2.0.0", "tasks": [{ "label": "gcc", "type": "shell", // { shell | process } // 适用于 Windows 的配置: "windows": { "command": "gcc", "args": [ "-g", "\"${file}\"", "-o", "\"${fileDirname}\\${fileBasenameNoExtension}.exe\"" // 设置编译后的可执行文件的字符集为 GB2312: // "-fexec-charset", "GB2312" // 直接设置命令行字符集为 utf-8: // chcp 65001 ] }, // 定义此任务属于的执行组: "group": { "kind": "build", // { build | test } "isDefault": true // { true | false } }, // 定义如何在用户界面中处理任务输出: "presentation": { // 控制是否显示运行此任务的面板。默认值为 "always": // - always: 总是在此任务执行时显示终端。 // - never: 不要在此任务执行时显示终端。 // - silent: 仅在任务没有关联问题匹配程序且在执行时发生错误时显示终端 "reveal": "silent", // 控制面板是否获取焦点。默认值为 "false": "focus": false, // 控制是否将执行的命令显示到面板中。默认值为“true”: "echo": false, // 控制是否在任务间共享面板。同一个任务使用相同面板还是每次运行时新创建一个面板: // - shared: 终端被共享,其他任务运行的输出被添加到同一个终端。 // - dedicated: 执行同一个任务,则使用同一个终端,执行不同任务,则使用不同终端。 // - new: 任务的每次执行都使用一个新的终端。 "panel": "dedicated" }, // 使用问题匹配器处理任务输出: "problemMatcher": { // 代码内问题的所有者为 cpp 语言服务。 "owner": "cpp", // 定义应如何解释问题面板中报告的文件名 "fileLocation": [ "relative", "\\" ], // 在输出中匹配问题的实际模式。 "pattern": { // The regular expression. "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", // 第一个匹配组匹配文件的相对文件名: "file": 1, // 第二个匹配组匹配问题出现的行: "line": 2, // 第三个匹配组匹配问题出现的列: "column": 3, // 第四个匹配组匹配问题的严重性,如果忽略,所有问题都被捕获为错误: "severity": 4, // 第五个匹配组匹配消息: "message": 5 } } }] }
launch.json:
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 // "program": "${fileDirname}/${fileBasenameNoExtension}.exe", // "program": "${workspaceFolder}/Demo/${fileBasenameNoExtension}.exe", "version": "0.2.0", "configurations": [ { "name": "(gdb) 启动", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "D:\\Program Files\\MinGW\\mingw64\\bin\\gdb.exe", "preLaunchTask": "gcc", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true } ] } ] }
1.2编译
本项目基于C语言实现,在配置好环境后,我们需要执行hello.c程序来观察环境是否配置完成。这里简单地输出了"hello world"。
1 #include <stdio.h> 2 3 int main() 4 { 5 printf("hello world!\n"); 6 }
2.模块化设计
写代码要小步快跑不断迭代,罗马不是一天建成的,不要期望一撮而就。我们应该采取这种思路,从 hello world 开始不断迭代调试使代码长的越来越像一个命令行的菜单小程序。在孟宁老师的代码中,就充分体现了这种思想。下图为menu代码的各个不同阶段,从简单的输出hello world,到最终形成菜单。
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出,没错就是Dijkstra最短路径算法的作者。
模块化软件设计应使每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。
为了体现模块化的思想,孟宁老师在代码lab3-3中引入了接口,将功能的实现与主代码程序分离开来。
在这个文件夹中,menu.c为主程序,linklist.h为功能实现程序的接口,linklist.c为功能实现的具体代码。主程序通过调用接口中的函数实现自己所需要的功能。好的代码应该实现了松散耦合和功能内聚。
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度,理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。
3.可重用接口
尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。
我们可以看到,在代码lab4的linktable.h中,插入了以下代码:
1 /* 2 * LinkTable Node Type 3 */ 4 typedef struct LinkTableNode 5 { 6 struct LinkTableNode * pNext; 7 }tLinkTableNode; 8 9 /* 10 * LinkTable Type 11 */ 12 typedef struct LinkTable 13 { 14 tLinkTableNode *pHead; 15 tLinkTableNode *pTail; 16 int SumOfNode; 17 pthread_mutex_t mutex; 18 }tLinkTable;
该代码结构代表链接的链表,而没有具体的值。这样做的目的是使得业务逻辑层和数据存储层分离,便于我们进行代码重用。有点类似于面向对象的语言特点。
可重用接口的设计除了方便我们重用代码之外,还可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。
4.线程安全
线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
在代码lab5.2中,我们可以看到代码引入了线程模块。
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include <pthread.h> 4 #include"linktable.h" 5 6 /* 7 * LinkTable Type 8 */ 9 struct LinkTable 10 { 11 tLinkTableNode *pHead; 12 tLinkTableNode *pTail; 13 int SumOfNode; 14 pthread_mutex_t mutex; 15 16 };
并且在后续代码中运用到了同步与互斥的思想,从而达到对线程的保护,实现线程安全。
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)); //P操作 14 pLinkTable->pHead = pLinkTable->pHead->pNext; 15 pLinkTable->SumOfNode -= 1 ; 16 pthread_mutex_unlock(&(pLinkTable->mutex)); //V操作 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 }
5.总结
通过学习孟宁老师的代码与课程,我对代码模块化设计、可重用接口、线程安全等议题有了更深的理解。代码需要从最基础开始,一步一步成长,这些思想能指导我在未来的编码中更加符合规范,不犯基础性的错误。