代码中的软件工程

参考资料

调试环境的配置和编译

安装MinGW和添加环境变量

  • Path环境变量,用于指定操作系统需要使用到的可执行程序的位置。c++的话要把MinGW(Minimalist GNUfor Windows包含GCC、GNU binutils 等工具,以及对等于 Windows SDK)的bin(binary包含用户的可执行文件)目录加入其中,从而使得os在任何目录下都可以使用其下的应用程序(gcc.exe, g++.exe)
  • 不同于C++,平常使用的Java不仅要添加Path环境变量,还需要添加JAVA_HOME(编译、运行Java程序时,JRE会去该变量指定的路径中搜索所需的类(.class)文件,通过javac命令 可以将java源文件编译为class字节码文件)。 而Path则是运行字节码文件;由java虚拟机对字节码进行解释和运行

安装C++扩展

安装一键运行扩展

配置jason文件

  1. lauch.json 用于指定调试语言环境,指定调试类型,执行编译好的文件

    需要注意的是,fileDirname是vscode自动生成的不需要添加啥环境变量(包含应用程序将使用到的信息,保证操作系统在任何位置都能找到响应的变量)miDebuggerPath与自己电脑中的gcc对应,externalConsole打开,个人习惯。

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "gcc.exe - 生成和调试活动文件",
                "type": "cppdbg",
                "request": "launch",
                "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
                "args": [],
                "stopAtEntry": false,
                "cwd": "${workspaceFolder}",
                "environment": [],
                "externalConsole": true,
                "MIMode": "gdb",
                "miDebuggerPath": "C:\\MinGWold\\mingw64\\bin\\gdb.exe",
                "setupCommands": [
                    {
                        "description": "为 gdb 启用整齐打印",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ],
                "preLaunchTask": "C/C++: gcc.exe build active file"
            }
        ]
    }
    
  2. tasks.json 用于对文件的编译

    {
        "tasks": [
            {
                "type": "cppbuild",
                "label": "C/C++: gcc.exe build active file",
                "command": "C:\\MinGWold\\mingw64\\bin\\gcc.exe",
                "args": [
                    "-g",
                    "${file}",
                    "-o",
                    "${fileDirname}\\${fileBasenameNoExtension}.exe"
                ],
                "options": {
                    "cwd": "C:\\MinGWold\\mingw64\\bin"
                },
                "problemMatcher": [
                    "$gcc"
                ],
                "group": {
                    "kind": "build",
                    "isDefault": true
                },
                "detail": "Generated task by Debugger"
            }
        ],
        "version": "2.0.0"
    }
    
  3. c_cpp_properties.json, 用于配置编译器环境的,包括启动器代号、位数(这些是自定义的)、编译选项、启动设置、编译模式等。

    	 {
        "configurations": [
            {
                "name": "Win32",
                "includePath": [
                    "${workspaceFolder}/**"
                ],
                "defines": [
                    "_DEBUG",
                    "UNICODE",
                    "_UNICODE"
                ],
                "compilerPath": "C:\\MinGWold\\mingw64\\bin\\gcc.exe",
                "cStandard": "gnu17",
                "cppStandard": "gnu++14",
                "intelliSenseMode": "gcc-x86"
            }
        ],
        "version": 4
    }
    

运行结果

代码分析

目录结构

  • 从目录结构中可以看出如下信息
    1. 很难看出该程序的逻辑,也不知道那部分为
    2. 具有模块化设计的思想, menu层主要负责和用户的交互,链表层模块存于linktable.c
  • 点开各个文件浏览每个文件的功能
    • linktable.c 为链码层的主要文件,定义了链表的节点LinkTable以及底层的创建删除添加搜素获取等功能
    • linktable.h则是为menu.c提供了可以使用的接口
    • menu.c定义了菜单的功能,回调函数、菜单的查询帮助等操作和用户数据节点DataNode
    • menu.h 为test.c 提供了能够使用的函数

软件工程的思想

本文的分析方式通过软件设计的重要设计指导原则进行分析

注释

/********************************************************************/
/* Copyright (C) SSE-USTC, 2012-2013                                */
/*                                                                  */
/*  FILE NAME             :  linktabe.c                             */
/*  PRINCIPAL AUTHOR      :  Mengning                               */
/*  SUBSYSTEM NAME        :  LinkTable                              */
/*  MODULE NAME           :  LinkTable                              */
/*  LANGUAGE              :  C                                      */
/*  TARGET ENVIRONMENT    :  ANY                                    */
/*  DATE OF FIRST RELEASE :  2012/12/30                             */
/*  DESCRIPTION           :  interface of Link Table                */
/********************************************************************/

/*
 * Revision log:
 *
 * Created by Mengning,2012/12/30
 * Provide right Callback interface by Mengning,2012/09/17
 *
 */
	

通过上述的注释可以看到,文件头部罕有项目名称,所属的模块以及主要的功能等。通过孟老师上课讲的内容可以知道,良好的代码要么可以通过函数的名称定义等轻松的理解其逻辑功能,要么有详尽清晰地注释。作为刚开始学习写工程化代码的我,自然要将代码的注释写的清晰详尽。因此学习了如何为头部添加合适的代码

  1. 下载插件

  1. 启动头部注释快捷键 window:ctrl+alt+i,mac:ctrl+cmd+i, linux: ctrl+meta+i
  2. 添加函数注释: window:ctrl+alt+t,mac:ctrl+cmd+t,linux: ctrl+meta+t

注释结构如下图所示

/*
 * @Author: your name
 * @Date: 2020-11-03 21:29:15
 * @LastEditTime: 2020-11-07 11:03:04
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: \menu\test.c
 */\\

模块化

通过menu项目的目录结构便可以看出系统的模块化设计思想,具体为把整个menu项目划分为两个模块,其中业务层模块主要负责面向用户这部分位于menu文件中,而链码层模块主要负责存储底层的数据,以及提供相应的操作,这部分位于linktable之中。进而实现了功能内聚。

接口

参照接口的五个基本要素,对照老师上课讲的GetLinkTableHead接口,自行对SearchLinkTableNode进行分析


tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
  • 接口的目的:

    设置SearchLinkTableNode的目的是为了在链码层中遍历寻找用户需要得到的信息 ,SearchLinkTableNode清晰地表明了该接口的目的

  • 接口的前置条件

    该接口的前置条件为(pLinkTable != NULL && Conditon != NULL)

  • 接口双方的通信规范

    该接口的通信规范是通过数据结构tLinkTableNode和tLinkTable以及回调函数int Conditon(tLinkTableNode * pNode, void * args)定义的

  • 接口的后置条件

    接口的后置条件为tLinkTableNode类型的指针作为返回值

  • 接口所隐含的质量属性

    接口的质量属性包括运行时的质量属性和开发时的质量属性,对于运行时质量属性,该接口需要在可以接受的延时范围内搜索到想要的结果,或者返回空值

    对于开发时的质量属性,开接口设计了Callback,从而实现了链码层和用户层的解耦,大大提高了可重用性。具体细节如下,

    int SearchCondition(tLinkTableNode * pLinkTableNode)
    {
        tDataNode * pNode = (tDataNode *)pLinkTableNode;
        if(strcmp(pNode->cmd, cmd) == 0)
        {
            return  SUCCESS;  
        }
        return FAILURE;	       
    }
    int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
    {
        char * cmd = (char*) args;
        tDataNode * pNode = (tDataNode *)pLinkTableNode;
        if(strcmp(pNode->cmd, cmd) == 0)
        {
            return  SUCCESS;  
        }
        return FAILURE;	       
    }
    

    相较于版本1的回调函数实现方式,本文中的menu项目采用了第二种方式。可以看出,版本1中的cmd为全局变量,而版本二通过传入参数的方式简化了接口的前置条件,从而提高了该接口开发时的质量属性

信息隐藏

如图所示,相较于写法二,本项目所采用的写法一更能优秀,原因是将底部链码层的具体定义方式隐藏在了.c之中,并不暴露给用户,从而降低

typedef struct LinkTable tLinkTable;  // 位于.h
struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int			SumOfNode;
    pthread_mutex_t mutex;

};                                    //位于.c  写法一
typedef struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int			SumOfNode;
    pthread_mutex_t mutex;
}tLinkTable;                          //位于.h   写法二

增量开发

增量开发和迭代开发的关系

通俗的讲,迭代开发是以实现用户的需求为目的,然后再一次次的迭代使得项目不断改进。而增量开发是从一开始就定义好最终需要交付产品的方案,分模块一次次的实现。即类似于下图的区别

增量开发:

迭代开发

通过下载孟宁老师开发过程的源码,从不同版本的迭代中看到了增量开发的过程:

lab1 实现了对C++项目的简单测试,保证hello程序可以正常运行,lab2实现了menu菜单最基础的功能即help和quit命令,lab3.1加入了version并结构化了(tDataNode)代码,lab3.2抽离出了FindCmd和ShowAllCmd等接口,lab3.3 则加入了简易底层的linklist。lab4则实现了linktable的接口,lab5.1加入了Condition回调函数,降低了不同模块的耦合度。lab5.2则是简化了回调函数的前置条件,利用void * args取消了对全局变量的依赖。并隐藏了LinkTable。lab7.1在删除节点时保证了函数的可重入性和线程安全,lab7.2的便是最后交付形式的代码。从上述过程中充分展出除了增量开发的特点。

函数的可重入性与线程安全

int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
    if(pLinkTable == NULL || pNode == NULL)
    {
        return FAILURE;
    }
    pthread_mutex_lock(&(pLinkTable->mutex));
    if(pLinkTable->pHead == pNode)
    {
        pLinkTable->pHead = pLinkTable->pHead->pNext;
        pLinkTable->SumOfNode -= 1 ;
        if(pLinkTable->SumOfNode == 0)
        {
            pLinkTable->pTail = NULL;	
        }
        pthread_mutex_unlock(&(pLinkTable->mutex));
        return SUCCESS;
    }
    tLinkTableNode * pTempNode = pLinkTable->pHead;
    while(pTempNode != NULL)
    {    
        if(pTempNode->pNext == pNode)
        {
            pTempNode->pNext = pTempNode->pNext->pNext;
            pLinkTable->SumOfNode -= 1 ;
            if(pLinkTable->SumOfNode == 0)
            {
                pLinkTable->pTail = NULL;	
            }
            pthread_mutex_unlock(&(pLinkTable->mutex));
            return SUCCESS;				    
        }
        pTempNode = pTempNode->pNext;
    }
    pthread_mutex_unlock(&(pLinkTable->mutex));
    return FAILURE;		
}

  • 对修改加了锁,在此过程中不允许线程的重入, 保证两个或多个线程同时访问该函数时不会产生冲突,
  • 在删除的过程中,pLinkTable->pHead = pLinkTable->pHead->pNext;删除操作一步完成,保证了链表不会因为删除过程中其它线程重入而导致链表的中断。从而保证了线程安全。
  • 线程安全和可重入的关系:可重入指的是函数可以由多于一个任务并发使用,而不必担心数据错误。线程安全是指多个线程同时运行该段代码,其结果与单线程相同。因此可重入函数不一定是线程安全的,因为多个可重入的函数在多个线程中并发使用时,会存在共享全局变量和静态变量导致的冲突问题。
posted @ 2020-11-07 14:38  neoth  阅读(136)  评论(0)    收藏  举报