从menu项目了解代码中的软件工程

0.前言

       本篇博客是由孟宁老师上课内容和所提供资料,在以VS Code + GCC工具集为主要环境,通过对孟宁老师提供的menu代码进行编译调试,了解软件工程中的模块化设计、可重用接口、线程安全等问题,了解代码一步一步如何健壮起来。

1.通过VSCode装C++及环境配置

1.1打开Visual Studio Code软件,在扩展的应用商店中下载安装c/c++。

 

1.2下载MinGW,安装MinGW,并配置环境变量。

打开此电脑的属性,选择高级选项,进入系统环境变量的选项,双击path,加入自己安装的MinGW的路径。

 打开 CMD 控制台终端,执行 gcc -v 和 gdb -v,安装成功。

1.3.修改vs调试配置文件

在vscode创建一个hello.c文件,利用F5快捷键进行运行。

 选择(GDB/LLDB),选择gcc.exe生产和调试活动文件

 执行命令后会产生tasks.json与launch.json文件,以下是默认配置。

    

要注意配置文件command是不是自己安装的路径,还需要将externalConsole改为true.

以下是修改后的tasks.json与launch.json

{
    "tasks": [
        {
            "type": "shell",
            "label": "C/C++: gcc.exe build active file",
            "command": "C:\\mingw64\\bin\\gcc.exe",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}\\${fileBasenameNoExtension}.exe"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ],
    "version": "2.0.0"
}
{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "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:\\mingw64\\bin\\gdb.exe",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "C/C++: gcc.exe build active file"
        }
    ]
}

 至此,关于VsCode上C/C++编译调试环境配置完成。

2、代码的成长

通过老师给予menu的文件,可以发现代码的成长过程,lab1仅仅只有输出"hello world",到后面能实现越来越多的功能,到最后能具有较为完整的菜单功能。

3.模块化设计

模块化设计是指在对一定范围内的不同功能或相同功能不同性能、不同规格的产品进行功能分析的基础上,划分并设计出一系列功能模块,通过模块的选择和组合可以构成不同的产品,以满足市场的不同需求的设计方法。这样做可以有效地降低程序的复杂度,使程序设计、调试和维护等操作变得简单化。

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

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

内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。 理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。

可以观察老师所给的lab3.1与lab3.3作为对比。

lab3.1只有一个menu.c文件,其代码如下:

#include <stdio.h>
#include <stdlib.h>

int Help();
int Quit();

#define CMD_MAX_LEN 128
#define DESC_LEN    1024
#define CMD_NUM     10

typedef struct DataNode
{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;

static tDataNode head[] = 
{
    {"help", "this is help cmd!", Help,&head[1]},
    {"version", "menu program v1.0", NULL, &head[2]},
    {"quit", "Quit from menu", Quit, NULL}
};

int main()
{
    /* cmd line begins */
    while(1)
    {
        char cmd[CMD_MAX_LEN];
        printf("Input a cmd number > ");
        scanf("%s", cmd);
        tDataNode *p = head;
        while(p != NULL)
        {
            if(strcmp(p->cmd, cmd) == 0)
            {
                printf("%s - %s\n", p->cmd, p->desc);
                if(p->handler != NULL)
                {
                    p->handler();
                }
                break;
            }
            p = p->next;
        }
        if(p == NULL) 
        {
            printf("This is a wrong cmd!\n ");
        }
    }
}

int Help()
{
    printf("Menu List:\n");
    tDataNode *p = head;
    while(p != NULL)
    {
        printf("%s - %s\n", p->cmd, p->desc);
        p = p->next;
    }
    return 0; 
}

int Quit()
{
    exit(0);
}

其代码的耦合度较高。而lab3.3中,除了menu.c文件外,还多了linklist.c与linklist.h文件。

linklist.c文件代码如下:

#include <stdio.h>
#include <stdlib.h>
#include "linklist.h"


tDataNode* FindCmd(tDataNode * head, char * cmd)
{
    if(head == NULL || cmd == NULL)
    {
        return NULL;        
    }
    tDataNode *p = head;
    while(p != NULL)
    {
        if(!strcmp(p->cmd, cmd))
        {
            return p;
        }
        p = p->next;
    }
    return NULL;
}

int ShowAllCmd(tDataNode * head)
{
    printf("Menu List:\n");
    tDataNode *p = head;
    while(p != NULL)
    {
        printf("%s - %s\n", p->cmd, p->desc);
        p = p->next;
    }
    return 0; 
}

可以发现他将原来lab3.1中一些模块放入linklist.c文件中,并通过linkli.h文件声明

typedef struct DataNode
{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;

/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tDataNode * head, char * cmd);
/* show all cmd in listlist */
int ShowAllCmd(tDataNode * head);

最后在menu.c文件中include此文件,就可使用模块方法,从而降低了耦合度,提高了灵活性。

#include <stdio.h>
#include <stdlib.h>
#include "linklist.h"

4、可重用接口

接口是双方共同遵守的一种协议规范,在软件系统内部通常是定义一组API函数约定模块之间的通信关系。

老师介绍两种函数接口方式,即Call-in方式的函数接口和Callback方式的函数接口。

给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。

在lab5中的listlink.h中加入了新的声明:

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode));

作为call-in方式函数。

menu.c文件中又新增了一SearchCondition函数:

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

利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性。

 在lab.5.2的代码中,发现call-in方式的函数接口SearchLinkTableNode增加了一个参数args,callback函数Conditon也增加了一个参数args。

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

为了降低风险增加了args参数。

5. 线程安全

可重入函数:

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

线程安全:

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

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

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

打开lab7.1的代码,发现在linktable.c文件中有定义了Linktable结构:

struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int            SumOfNode;
    pthread_mutex_t mutex;

};

其中定义了 mutex信号量,由所学知识可知,信号量可以用来进行多个进程间的同步与互斥。

在linktable.c文件下有删除结点的函数:

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

在删除结点时先将临界区上锁,防止其他线程使用,结束后执行解锁操作。

 6.小结

通过阅读老师提供的代码,不仅认识到了代码一步一步健壮的过程,还在代码中引入了软件工程的重要思想,例如工程中的模块化设计、可重用接口、线程安全等问题,这些问题要在之后自己编写代码中加以注意实践。

7.参考资料

https://github.com/mengning/menu 

 

posted @ 2020-11-04 23:53  smilekiller  阅读(157)  评论(0)    收藏  举报