代码中的软件工程

前言

首先非常感谢孟宁老师的悉心教导。经过孟宁老师课堂教学后,对于孟老师通过一个简单的menu小程序,直观细致地给我们讲解了代码规范、模块化设计、可重用接口以及线程安全等问题有了愈加深入的了解,让我受益匪浅。

以下学习内容都来自于以下孟宁老师的个人库和gihub: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

https://github.com/mengning/menu 

一.C/C++编译调试环境配置

首先,安装VSCODE软件。

打开VSCODE进行C/C++环境配置,搜索下载C/C++如下图所示

  

安装了C/C++扩展包之后,C/C++ extension不包含C++编译器和调试器,需要额外安装编译调试工具,选择安装MinGW 。

选择谷歌浏览器进入http://mingw.org/ 进行下载。

 

 

 Architecture选择x86_64,适合64位系统,其他默认即可

安装完成后,为其在系统中添加环境变量。

打开此电脑-属性-高级系统设置-高级-环境变量

 

 

 

 成功配置环境变量后,打开CMD命令行输入gcc -v 和gdb -v检查版本和情况。

 

 

 

 

 

 

打开VSCODE,直接建立一个hellogo.cpp,

然后选择运行和调试,会弹出一个选择 C++(GDB/LLDB),再弹出选择 g++.exe,之后会自动生成 launch.json 配置文件 和tasks.jason

配置launch.jason文件,tasks.jason,使用ctrl+shfit+p 寻找一个c_cpp_properties.jason文件,加载即可,三个文件加载如下即可。

 

 

 

launch.jason文件如下

 

 

 

运行hellogo.cpp,输出成功如下图所示。

 

 

二.代码规范与代码风格

代码的简洁、清晰、无歧义”的基本原则

 

1一般.要采用缩进风格

 

2.一般情况下,源程序有效注释量必须在20%以上。

 

3.文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、修改日志等。

 

4.函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值等。注释格式尽量统一,建议使用“/* …… */”。

 

5.避免使用不易理解的数字,用有意义的标识来替代。

 

6.不要使用难懂的技巧性很高的语句,除非很有必要时。

 

7.不允许把多个短语句写在一行中,即一行只写一条语句。

 

 

三. 模块化设计

 

定义:模块化程序设计是指在进行程序设计时将一个大程序按照功能划分为若干小程序模块,每个小程序模块完成一个确定的功能,并在这些模块之间建立必要的联系,通过模块的互相协作完成整个功能的程序设计方法。

 

思想:在设计较复杂的程序时,一般采用自顶向下的方法,将问题划分为几个部分,各个部分再进行细化,直到分解为较好解决问题为止。模块化设计,简单地说就是程序的编写不是一开始就逐条录入计算机语句和指令,而是首先用主程序、子程序、子过程等框架把软件的主要结构和流程描述出来,并定义和调试好各个框架之间的输入、输出链接关系逐步求精的结果是得到一系列以功能块为单位的算法描述。以功能块为单位进行程序设计,实现其求解算法的方法称为模块化。模块化的目的是为了降低程序复杂度,使程序设计、调试和维护等操作简单化。

 

软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

 

耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。

 

孟宁老师特别强调,软件工程要追求低内聚和高耦合。

 

将数据结构和它的操作与菜单业务之间进行分离处理就是一个典型的模块化代码,下面来看看源码的模块化特点。

一般会用一个文件将定义好的数据结构 放在里面,数据结构里面一般会包括一些变量或者指针,还有一些函数声明,例如下面的LinkedList.h

 

 

 

 

在另外一个后缀.c的文件,调用上面对应的数据结构,开始对数据结构的内部函数的声明进行具体的定义,具体的逻辑实现。

 

 

 

 

 

 

 

 进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。

 

四.可重用接口

什么是接口?

接口的概念

接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。接口规格包含五个基本要素,分别是接口的目的;接口使用前所需要满足的条件,一般称为前置条件或假定条件; 使用接口的双方遵守的协议规范; 接口使用之后的效果,一般称为后置条件; 接口所隐含的质量属性

项目中的可重用接口设计
我们用到了链表这种数据结构,而在一些比较大的项目中,我们可能会多次用到链表。在后面只需要通过调用接口来实现链表的操作即可,无需关注内部实现和细节。

可以在源码中看到下面的数据结构,实际上C语言的严格意义上的接口,严格说应该是通过把通过调用函数,把不同的参数传到同一个调用参数里面进行操作。

typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode;

 

  

/*
 * get LinkTableHead
 */
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);

 

以上代码就是一个C语言的函数接口

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

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

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

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

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

 

一般地,我们尽量把内部细节隐藏好,只需要给出足够明显的调用参数即可,有效地提高了接口的通用性.这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据,以下都会一些对链表节点进行操作的接口,只需要把要操作的目的节点放进接口函数的对应参数列表即可。

int  DeleteLinkTable(tLinkTable * pLinkTable){


}
/ *
 *将LinkTableNode添加到LinkTable
 * /
int  AddLinkTableNode(tLinkTable * pLinkTable,tLinkTableNode * pNode){

}
;
/ *
 *从LinkTable删除LinkTableNode
 * /
int  DelLinkTableNode(tLinkTable * pLinkTable,tLinkTableNode * pNode){

}
;

五.线程安全

不同的线程的如果不按某种规则进行操作的话,可能会出现内存错误,读写错误等等问题。例如同时对某一个地址进行写操作,例如需要先读取一个地址的内容,但却在读取的过程中提前被另一个线程进行写的修改了,造成读取信息的错误。所以需要设置全局变量或者静态变量或者互斥变量对可能会造成冲突的线程的同步和异步顺序进行统一的管理。

首先要逐一分析所有的函数是不是都是可重入函数;

  • 函数有没有访问临界资源(全局变量、静态存储区等),如果有访问临界资源的话需要仔细分析竞争互斥的处理能不能有效避免临界资源冲突问题。
  • 对于不可重入函数要具体分析其对线程安全带来的影响,有没有潜在的破坏性。实际上软件测试技术及软件质量保证体系的发展已经无奈地默许软件缺陷的存在

可能会出现的冲突情况:

1.写互斥

2.读写互斥

对于保证线程安全和保证程序按规则运行的PV算法一般有,读者写者算法,生产者消费者,哲学家算法,银行家算法等等。下面就简单介绍孟宁老师源码上最基本的读写锁操作。

/*
 * Add a LinkTableNode to LinkTable
 */
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
    if(pLinkTable == NULL || pNode == NULL)
    {
        return FAILURE;
    }
    pNode->pNext = NULL;
    pthread_mutex_lock(&(pLinkTable->mutex));    //加锁
    if(pLinkTable->pHead == NULL)
    {
        pLinkTable->pHead = pNode;
    }
    if(pLinkTable->pTail == NULL)
    {
        pLinkTable->pTail = pNode;
    }
    else
    {
        pLinkTable->pTail->pNext = pNode;
        pLinkTable->pTail = pNode;
    }
    pLinkTable->SumOfNode += 1 ;
    pthread_mutex_unlock(&(pLinkTable->mutex));    //释放锁
    return SUCCESS;        
}

需要对链表操作的时候,设置一个锁,把锁关上,这期间静止其他线程对链表进行操作,本线程操作完成后,把锁释放。完成读写互斥和写写互斥。

 

总结:

通过学习了孟宁老师的这一课后,使我更深刻的了解到了软件开发过程中,模块化设计,隐藏细节,只暴露接口的重要性。可重用接口,只需要调用函数传入参数简化操作的简化性。运行多线程时,需要加上某些锁来保证线程的安全进行的安全性。学习完成之后,受益匪浅,对软件工程有了更深一层的认识。

posted @ 2020-11-07 19:54  姚金甫  阅读(197)  评论(0)    收藏  举报