代码中的软件工程

首先感谢孟宁老师的教学指导。这次的项目用的是孟老师的menu项目。

参考 https://github.com/mengning/menu

   https://mp.weixin.qq.com/s/KJ4oU5ggccu1f5mMaEo1eg

 

基于 VS Code 的 C/C++开发调试环境配置

 

打开 VSCode 点击最左侧的管理扩展插件图标(Ctrl+Shift+X),如下图在扩展插件市场里搜索 C++,找到 Microsoft C/C++扩展插件“C/C++ for Visual Studio Code”,点击 Install 安装即可。

安装 C/C++编译器和调试器。VSCode 的 C/C++插件并不包含 C/C++编译器和调试器,我们需要自己安装 C/C++编译器和调试器,如果您机器上已经安装过 C/C++编译器和调试器可以直接使用。常用的 C/C++编译器和调试器如下:Mingw-w64 is GCC for Windows 64 & 32 bits - http://mingw-w64.org。不同的 C/C++编译器和调试器的用法有所不同,由于 GCC 在不同平台上都可以使用,而且用法基本一致,我们这里选用 Mingw-w64(包含 GCC 和 GDB)用于 Windows 环境

 

Windows 环境下安装 Mingw-w64

MinGW(Minimalist GNU for Windows), 是一个适用于微软 windows 应用程序的极简开发环境。MinGW 提供了一个完整的开源编程工具集,适用于原生 MS-Windows 应用程序的开发,并且不依赖于任何第三方 C 运行时 DLL。MinGW 主要供在 MS-Windows 平台上工作的开发人员使用,也可以跨平台使用。Mingw-w64 是原始 mingw.org 项目的升级版,该项目旨在支持 Windows 系统上的 GCC 编译器。它在 2007 年进行了分支,以便为 64 位和新 API 提供支持。从那以后,它得到了广泛的使用和分发。

配置 Visual Studio Code 构建任务

一般在命令行下使用"code ."命令可以打开及当前文件夹,同时将当前文件夹作为工作区(workspace)。同时 VS Code 会在当前工作区创建.vscode 文件夹并在其中创建三个 JSON 配置文件:

通过菜单 Terminal 选择 Configure Default Build Task... 或者 Configure Tasks... 然后选择 C/C++: gcc build active file,在当前项目目录(工作区)下自动生成.vscode/tasks.json 配置文件,构建任务的简要配置范例 tasks.json 如下,其中"command"是指明编译器;"args"是编译器 gcc 的参数;"isDefault"为 true 表示同时按 Ctrl+Shift+B 快捷键自动执行默认构建任务(Default Build Task)。

 配置文件 launch.json 用于告诉 VS Code 如何调用调试器调试程序,我们这里 GDB debugger 为例。配置调试环境可以通过左侧的“启动和调试”图标或者快捷键 Ctrl+Shift+D 进入 Debug 二级菜单,然后创建一个 launch.json 配置文件(create a launch.json file),选择 C++(GDB/LLDB)

触发程序调试的方法是通过菜单 Run 选择 Start Debugging 或者直接按 F5 键开始调试程序。

这是写的程序和运行结果:

                       

 

代码规范与代码风格

 

正如孟老师在课件中写的那样:罗马不是一天建成的,不要期望一蹴而就。可以看到项目包中的menu的程序也是逐渐成长,从lab1到lab7展现了我们先在一个hello文件得到启发,逐步引入规范化编码、引擎、模块化思想、可重用接口和线程安全等内容,体现了软件工程的思想,使得程序更加的健壮。

在整个代码中可以见到函数声明和定义是分开放置的,这样做的好处是:

编译速度:将所有包含的文件连接在一起然后进行解析时,减少包含文件中代码的数量和复杂性将缩短编译时间。

避免代码重复/内联:如果您在头文件中完全定义了一个函数,则包含该头并引用该函数的每个目标文件都将包含该函数的自身版本。附带说明一下,如果要进行内联,则需要将完整的定义放入头文件中(在大多数编译器中)。

封装/清晰度:一个定义良好的类/函数集以及一些文档应足以供其他开发人员使用您的代码。 (理想情况下)不需要他们了解代码的工作原理-那么为什么要求他们筛选代码呢? (当然,相反的说法是,当需求仍然存在时,对于他们访问实现可能会很有用)。

在此模块中的具体体现就是:在.h文件中进行函数声明,在.c文件中进行具体的函数实现。如下图:

menu.h

menu.c

规范化的注释

注释也要使用英文,不要使用中文或特殊字符,要保持源代码是ASCII字符格式文件;不要解释程序是如何工作的,要解释程序做什么,为什么这么做,以及特别需要注意的地方;每个源文件头部应该有版权、作者、版本、描述等相关信息。

在代码中的具体体现:

 

代码规范:

•缩进:4个空格;

•行宽:< 100个字符;

•代码行内要适当多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d));

•在一个函数体内,逻揖上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行以示区隔;

•在复杂的表达式中要用括号来清楚的表示逻辑优先级;

•花括号:所有 ‘{’ 和 ‘}’ 应独占一行且成对对齐;

•不要把多条语句和多个变量的定义放在同一行;

•命名:合适的命名会大大增加代码的可读性;

•类名、函数名、变量名等的命名一定要与程序里的含义保持一致,以便于阅读理解;

•类型的成员变量通常用m_或者_来做前缀以示区别;

•一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter;

•类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写;

•类型、类、变量一般用名词或者组合名词,如Member

•函数名一般使用动词或者动宾短语,如get/set,RenderPage;

 

模块化

 

在软件设计中最重要的就是模块话的设计。通过模块化的设计让系统内的各个部分保持相对对了,以便每一个部分可以被独立地进行设计和开发。模块化设计的原理是关注点分离,相当于把复杂的问题分解成一个个简单的问题。软件设计中的模块化程度是一个软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。要追求低内聚和高耦合。

模块化遵循的原则

  • 一行代码只做一件事
  • 一个代码块只做一件事
  • 一个函数只做一件事
  • 一个软件模块只做一件事

我们可以看出,主要分为三个模块:test menu linktable 其中test作为程序的入口和添加命令操作的地方,它完全于menu隔开,当我们需要添加新的命令的时候,我们只需要在test模块中进行修改尔不用涉及到menu中的代码。同样的,当数据结构需要跟换的时候,我可以直接修改linktable中的代码而不用涉及menu。

 

可以看到lab3.3相比较lab3.2,多出了linklist.c和linklist.h两个文件。在lab3.3中,我们将menu中运用到的关键数据结构链表定义和函数操作申明在了linklist.h文件中,将对链表的操作的具体实现在了linklist.c文件中,主程序menu.c中的操作逻辑并没有变化。这看似好像是多此一举的操作,但是实际上却初具了软件工程中的模块化思想,我们将原本较为复杂的主程序进行了模块化的划分,主程序中只包含了引擎(while循环),将其余复杂的问题以模块、接口的方式提供给主程序,方便我们进行代码维护。模块化的好处就是我们可以将关注点进行分离,模块内部是如何实现的我们是不关心的,只需要专注于当前的问题就可以了。

  

 

 可重用接口

 

在软件开发中,由于不同的环境和功能要求,我们可以通过对以往成熟软件系统的局部修改和重组,保持整体稳定性,以适应新要求。这样的软件称为可重(chong)用软件。目的是节约软件开发成本,真正有效地提高软件生产效率。重用分为消费者重用和生产者重用,消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。生产者重用需要重点考虑设计通用的模块,通用的接口等因素。

接口的概念

接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。

接口规范包含的五个基本要素:

  • 接口的目的
  • 接口使用前所需要满足的条件,称前置条件或假定条件
  • 使用接口的双方遵守的协议规范
  • 接口使用之后的效果,称后置条件
  • 接口所隐含的质量属性

以下是一个软件模块接口举例。在lab4之后可以发现一个新的数据结构

 代表链接的链表,而没有具体的值。同时新增了一些函数API,我们拿一个来举例

  •该接口的目标是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标;

  •该接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL;

  •使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;

  •使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;

  •该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;

lab5中的linktable.h中新增了一个SearchLinkTableNode函数,这是一个acll-in方式的函数;而在lab5中的menu.c文件中又新增了一SearchCondition函数,这是一个callback方式函数(如下所示)。利用这种方式就可以使得linktable接口变得更加通用。

 

读到lab5,发现一个方法

 孟老师说过,利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性.这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。

 

 线程安全概念

 

线程是操作系统中的概念,是比进程更小的能够运行和调度的最小单位。在引入线程的操作系统中,线程是最小的执行单位,进程是最小的资源分配单位,可以更好的提高程序的吞吐率[5]。在提升了系统性能的同时,与进程存在的同样的安全问题需要解决,就是线程对资源临界区的访问安全性问题,依然要遵循信号量机制,从而实现线程之间的同步、互斥等访问。

孟老师说过:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。总的来说呢,就是要利用读写锁来实现线程安全

在lab5文件夹的linktable中,我们可以看到引入了头文件#include <pthread.h>,并且在链表数据结构中,增加了一个信号量mutex:

在lab5中,在DeleteLinkTable中,在while循环中访问链表时,就有lock操作和unlock操作。这里将链表作为了一种临界资源,需要按照信号量机制,对其进行互斥访问,即PV操作,保障线程安全。具体的实现例子如下所示:

 

 

 

总结

 

再次感谢孟宁老师的指导以及在Github上提供的源码。现总结如下:保持好的代码风格规范,能够对易读性有十分大的提升。注释、缩进、空格,命名规范,都需要注意。模块化的设计有助于提升质量属性,使得代码易于维护,提高代码功能的内聚程度。使用可重用的接口,隐藏了底层不必要接触的细节,使得我们有更多精力集中于业务层的处理。理论需要实践才能更好的掌握,要在日常的训练中将所学的知识付诸实践,提升代码规范,养成良好软件工程习惯。

 

posted @ 2020-11-08 00:17  ITloveyang  阅读(246)  评论(0)    收藏  举报