代码中的软件工程

参考资料:https://mp.weixin.qq.com/s/sU9Wh12JZqQyJfEBBafFAQ

      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

 

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

   

1.1 下载安装 Visual Studio Code

  详情可参考资料https://mp.weixin.qq.com/s/sU9Wh12JZqQyJfEBBafFAQ

 

1.2 安装 C/C++插件

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

  

 

 

1.3 安装 C/C++编译器和调试器

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

  1.3.1 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 提供支持。从那以后,它得到了广泛的使用和分发。

    在 Windows 环境下可以到通过链接 https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe 下载 Mingw-w64 的安装文件 mingw-w64-install.exe。

    下载并运行MinGW-W64-install.exe如下图所示:

       

 

  安装 MinGW-W64-install.exe 过程中有几个选项需要说明:

    lVersion 制定版本号,从 4.9.1-9.x.0,按需选择,没有特殊要求就用最新版吧;

    lArchitecture 跟操作系统有关,64 位系统选择 x86_64,32 位系统选择 i686;

    lThreads 设置线程标准可选 posix 或 win32,为了与其他平台保持一致,这里我们选择 posix;

    lException 设置异常处理系统,x86_64 可选为 seh 和 sjlj,i686 为 dwarf 和 sjlj;

    lBuild revision 构建版本号,选择最大即可。

  1.3.2  配置 Mingw-w64 环境变量

    我的电脑 -->  属性  -->  高级系统设置  -->  环境变量  -->  系统变量

      将 Mingw-w64 的安装目录下的 bin 目录的路径添加到系统变量的 path 选项中,点击确认即可,添加环境变量后,打开 CMD 控制台终端,执行 gcc -v 和 gdb -v 看看是否安装成功。

    cmd: 执行结果如下,表示安装成功:

      

  1.3.3  在vscode中配置c/c++扩展  

    编译器路径:用于生成项目来启用更准确的 IntelliSense 的编译器的完整路径,例如 /usr/bin/gcc。扩展将查询编译器以确定要用于 IntelliSense 的系统是否包含路径和默认定义。   
    IntelliSense 模式:要使用的 IntelliSense 模式,该模式映射到 MSVC、gcc 或 Clang 的体系结构专属变体。如果未设置或设置为 ${default},则扩展将选择该平台的默认值。Windows 默认为 msvc-x64,Linux 默认为 gcc-x64,macOS 默认为 clang-x64。选择特定 IntelliSense 模式以替代 ${default} 模式。

    

  1.3.4  配置 Visual Studio Code 构建任务

    通过菜单 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)。   

    tasks.json 配置文件是用来告诉 VS Code 如何编译程序,我们这里可以通过修改 tasks.json 配置构建任务,实际上就调用 gcc 编译器将源代码变异成可执行程序。

我们可以使用如下 hello.c 的代码用于测试 tasks.json 构建任务是否可以正常工作。

    task.json代码:    

{
    // 有关 tasks.json 格式的文档,请参见
    // https://go.microsoft.com/fwlink/?LinkId=733558
    "version": "2.0.0",
    "tasks": [
        {
            "type": "shell",// 任务执行的是shell命令
            "label": "gcc build active file",// task的名字是build,在launch.json内根据此任务名调用此任务;
            "command": "gcc",// 执行的具体指令
            "args": [// 如果上述的shell命令需要对象,则在这里添加,不需要可直接删掉;
                "-g",
                "${file}",//当前文件名
                "-o",//对象名,不进行编译优化
                "${fileDirname}/${fileBasenameNoExtension}"//当前文件名(去掉扩展名)
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

   1.3.5  配置 Visual Studio Code 调试环境

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

    launch.json  代码:     

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc build and debug active file",
            "type": "cppdbg",
            "request": "launch",// 执行当前文件
            "program": "${fileDirname}/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": true,// 选为true则会在打开控制台后停滞,暂时不执行程序,一般选false
            "cwd": "${workspaceFolder}", //当前执行程序的路径
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "gcc build active file",//task的名字,一定要跟上述的task名字对应好
            "miDebuggerPath": "D:\\Program Files\\mingw-w64\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\mingw64\\bin\\gdb.exe"
        }
    ]
}

 

1.4  在vscode中运行hello.c程序,测试环境。

    运行成功,说明环境配置成功。

  

 

 2, 代码中用到的软件工程知识

 

  2.1  良好的注释风格

    1,注释和版权信息:注释也要使用英文,不要使用中文或特殊字符,要保持源代码是ASCII字符格式文件;

     2,不要解释程序是如何工作的,要解释程序做什么,为什么这么做,以及特别需要注意的地方;

     3,每个源文件头部应该有版权、作者、版本、描述等相关信息。

      

 

   2.2  模块化软件设计

   2.2.1  什么是模块化?

     模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出。关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。

   2.2.2  如何衡量模块化程度?

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

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

      一般在软件设计中我们追求松散耦合。

      

 

       内聚度:是指一个软件模块内部各种元素之间互相依赖的紧密程度。

      理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。

   2.2.3  模块化在代码中的运用

    模块化设计在menu案例中具体表现为,不同的代码模块完成不同的任务,各模块间使用接口来完成合作,减少模块间的耦合。模块化代码遵循KISS原则,即

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

    menu案例主要分为三个大的模块,大模块又由小模块构成。menu中的三个模块代码分别为:linktable.c ,menu.c ,test.c 。其中linktable封装最基本的操作,例如:增、删、改、查等,linktable向menu部分提供接口,menu层主要负责菜单逻辑,通过接口调用 linktable 提供的各种功能函数来协同完成某一菜单功能。而test主要负责整合 menu中的各个菜单功能,使整个程序能够跑起来,完成必要的功能。各模块代码示例如下:

  linktable.c  中的代码:该小模块仅仅实现添加节点的功能,不做其他任务,体现了模块化设计中 一个函数只做一件事  的思想。

/*
 * 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;        
}

  

  menu.c中的代码: 该模块代码的功能就是向菜单中添加一条新的命令,代码通过调用 linktable 提供的AddLinkTableNode()函数,来完成菜单功能,通过接口来调用。  

/* add cmd to menu */
int MenuConfig(char * cmd, char * desc, int (*handler)())
{
    tDataNode* pNode = NULL;
    if ( head == NULL)
    {
        head = CreateLinkTable();
        pNode = (tDataNode*)malloc(sizeof(tDataNode));
        pNode->cmd = "help";
        pNode->desc = "Menu List";
        pNode->handler = Help;
        AddLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    pNode = (tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd = cmd;
    pNode->desc = desc;
    pNode->handler = handler; 
    AddLinkTableNode(head,(tLinkTableNode *)pNode);
    return 0; 
}

  

  test.c代码示例:该模块的唯一任务就是使系统能够运转起来。体现了模块化设计中   一个软件模块只做一件事 的思想。  

int main()
{
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("time","Show System Time",Time);
    MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
    ExecuteMenu();
}

 

   2.3   可重用软件设计

    2.3.1  接口的基本概念

      接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。

     在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。

    2.3.2  接口规格包含五个基本要素

       接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:

        接口的目的;

        接口使用前所需要满足的条件,一般称为前置条件或假定条件;

        使用接口的双方遵守的协议规范;

        接口使用之后的效果,一般称为后置条件;

        接口所隐含的质量属性。

    2.3.3  接口在代码中的运用

      例如定义在  linktable.h  中的如下接口:

        

      该接口的目标是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标; 该接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL; 使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的; 使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件; 该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;

       linktable.h 中声明了很多接口,这些接口在  linktable.c  中实现:

        

 

  2.4  可重入函数与线程安全

    2.4.1  线程的基本概念

      线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。

    2.4.2  可重入函数的基本概念

      可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。

    2.4.3  函数的可重入性与线程安全之间的关系

      可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题; 不可重入的函数一定不是线程安全的。

    2.4.4  线程安全在代码中的体现

      线程安全方面主要关注的问题如下:

        所有的函数是不是都是可重入函数

        不同的可重入函数有没有可能同时进入临界区

          写互斥

          读写互斥

      linktable中关于线程安全的代码分析: 

        pthread_mutex_lock(&(pLinkTable->mutex));//给临界区资源上锁,进行互斥访问

        pthread_mutex_unlock(&(pLinkTable->mutex));//释放临界区资源,允许其他线程访问

        pthread_mutex_destroy(&(pLinkTable->mutex));//销毁释放   

/*
 * Delete a LinkTable
 */
int DeleteLinkTable(tLinkTable *pLinkTable)
{
    if(pLinkTable == NULL)
    {
        return FAILURE;
    }
    while(pLinkTable->pHead != NULL)
    {
        tLinkTableNode * p = pLinkTable->pHead;
        pthread_mutex_lock(&(pLinkTable->mutex));
        pLinkTable->pHead = pLinkTable->pHead->pNext;
        pLinkTable->SumOfNode -= 1 ;
        pthread_mutex_unlock(&(pLinkTable->mutex));
        free(p);
    }
    pLinkTable->pHead = NULL;
    pLinkTable->pTail = NULL;
    pLinkTable->SumOfNode = 0;
    pthread_mutex_destroy(&(pLinkTable->mutex));
    free(pLinkTable);
    return SUCCESS;        
}

 

心得体会:

    感谢孟老师的精彩讲解,再结合本次实验,我收获颇多,动手能力大大提高。能够自主分析一些代码,并且将代码和软件工程方法相结合,学会了如何规范编码,了解了模块化设计思想,可重用接口以及线程安全等知识,对以后的开发学习意义重大,影响深远。

    

 

 

 

 

     

 

 

 

 

 

 

    

 

 

 

 

 

 

 

 

  

  

 
posted @ 2020-11-09 12:49  junc0125  阅读(331)  评论(0编辑  收藏  举报