代码中的软件工程

编译和调试环境配置

安装C/C++ 插件

C/C++ extension

安装C/C++编译器

C/C++插件只是让vs code具有处理C++的操作,但是具体的编译和debug还是需要专用的编译器来完成。编译器的种类有很多,这里采用孟宁老师推荐的Mingw-w64/GCC。

  1. 在浏览器输入https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe/download下载文件。
  2. 双击exe文件进行安装。

  3. 查看是否安装成功
  4. 运行代码
    代码
    运行结果

代码理解与分析

我们结合孟宁老师的menu项目的每一次迭代来理解软件开发的各种思想,沿着如下的路径:讲一讲每一次迭代解决了什么问题,还存在什么问题,然后下一次迭代进行解决。

初始(lab2)

首先看看lab2:

int main()
{
    char cmd[128];
    while(1)
    {
        scanf("%s", cmd);
        if(strcmp(cmd, "help") == 0)
        {
            printf("This is help cmd!\n");
        }
        else if(strcmp(cmd, "quit") == 0)
        {
            exit(0);
        }
        else
        {
            printf("Wrong cmd!\n");
        }
    }
}

lab2通过 if... else... 语句来遍历所有的可能的命令。如果我们此刻有一千种cmd命令,就需要一千行if... else...语句。很显然,这不利于后期的维护。

解耦(lab3.1、lab3.2)

为了解决lab2的if地狱,我们要对项目进行解耦。以孟宁老师lab3为例,结合代码来展示如何进行解耦:

lab3.1

为了不使用太多的if...else...判断, 我们换一种思路:把各种命令放到一个链表中,每次找到对应的节点,然后执行节点对应的操作(hanlder函数):

//节点如下,包含 cmd名称和具体的操作函数handler
typedef struct DataNode
{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;

然后我们看一下lab3.1中的main()help()

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

两个函数直接在内部对链表进行操作时,看起来很合理,但是随着我们项目逐渐发展壮大后,会有很多问题,比如:

  • 代码显得十分乱,每次阅读代码都要仔细查看和理解对链表做了什么操作。
  • 每当我们需要新增一个对链表进行查询的功能,那我们就要在新的函数中将链表查询的操作再写一遍,这样会很费力。
  • 当cmd命令数量足够多时,我们觉得链表查找效率太低了,想用树的结果来存储,那么我们除了要改变DataNode 的结构,我们还要改变每一个使用DataNode的函数的代码。一旦后期项目变得很大,这个任务是非常重的。

lab3.2

为了解决lab3.1的耦合太深问题,我们对其进行解耦,将对链表的操作抽离出去:

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

当我们要使用的时候,直接调用函数即可:

int main(){
     ....
      tDataNode *p = FindCmd(head, cmd);
     ...
}
int Help()
{
    ShowAllCmd(head);
    return 0; 
}


这样就可以解决上面的三个问题了:

  • 对链表操作的函数无需关注细节,直接调用就好,代码易于理解(前提是良好的语义化命名函数)
  • 如果新增查找链表的功能,直接调用函数即可,无需重写代码。
  • 如果我们想把链表改成树结构,改变链表和函数即可,基本不需改变调用者。

现在的代码仍然有不足之处,比如:

  • 如何工期紧张,我们想要两个人分工开发。但是现在必须完成链表的全部代码,才能写menu的代码,这样即使分工也和一个人的时间差不多(甚至一个人写还减少了交流成本)。
  • 如果其他项目或者这个项目的其他部分想要使用链表的内容,那么只能Ctrl cCtrl v了,无法很好地实现代码复用。

为了解决这一问题,我们需要进行可重用设计。

可重用(lab3.3)

为了实现代码复用和分工,我们把链表操作分离到一个单独的文件,并且用头文件(.h)作为外部接口来告诉别的文件如何使用链表操作。

//linklist.h
tDataNode* FindCmd(tDataNode * head, char * cmd);
/* show all cmd in listlist */
int ShowAllCmd(tDataNode * head);

在menu中使用时,导入头文件即可使用:

//menu.c
#include "linklist.h"
int main(){
  ...
   tDataNode *p = FindCmd(head, cmd);
  ...
}
int Help()
{
    ShowAllCmd(head);
    return 0; 
}

这样一来,如果两个开发者要分工开发,只需要一个商量好 头文件(.h),然后就可以各自按照头文件的规范进行开发,互不干扰,可以大大缩短开发的时间。

在单线程条件下,现在的代码已经堪称“完美”。但是一旦出现在多线程情况下,就会出现多种问题,比如线程a在链表中加入了一个新命令,但是数据还没从缓存写入内存,这时候线程b也在链表加入了一个新命令,线程a的修改就被覆盖了。

线程安全

为了实现线程安全,孟宁老师在menu中采用了加 互斥锁 的方式。在加锁期间,其他线程不能访问修改该数据。
pthread_mutex_t的用法如下:

//初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex)
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

linktable.c 中,当初始化 tLinkTable时,初始化锁:

tLinkTable * CreateLinkTable()
{
    ......初始化链表
    //初始化异步锁
    pthread_mutex_init(&(pLinkTable->mutex), NULL);
    return pLinkTable;
}

当对链表进行增删改时加锁:

//删除链表
int DeleteLinkTable(tLinkTable *pLinkTable)
{
    ....
        //当删除每一个结点时,加锁,改变链表后,解锁
        pthread_mutex_lock(&(pLinkTable->mutex));
        pLinkTable->pHead = pLinkTable->pHead->pNext;
        pLinkTable->SumOfNode -= 1 ;
        pthread_mutex_unlock(&(pLinkTable->mutex));
        free(p);
    }
    ......

    //销毁锁
    pthread_mutex_destroy(&(pLinkTable->mutex));
    free(pLinkTable);
    return SUCCESS;		
}
//删除链表节点
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
   ......
    //加锁
    pthread_mutex_lock(&(pLinkTable->mutex));
     .....
        //寻找到节点,删除节点,然后解锁
        pthread_mutex_unlock(&(pLinkTable->mutex));
        ....
    //找不到节点,解锁,返回失败
    pthread_mutex_unlock(&(pLinkTable->mutex));
    return FAILURE;		
}
//增加链表节点
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
    .....
    //加锁
    pthread_mutex_unlock(&(pLinkTable->mutex));
    ......
     //找到对应的位置,插入节点
    pLinkTable->pTail = pNode
    pLinkTable->SumOfNode += 1 ;
    //解锁
    pthread_mutex_unlock(&(pLinkTable->mutex));
    return SUCCESS;		
}

孟宁老师的menu代码中还有很多值得我们学习,比如如何注释,如何进行测试,makefile等等。但是由于篇幅有限,本篇博客就不再详细解读这部分内容了。

posted @ 2020-11-08 15:15  loser_wang  阅读(284)  评论(0编辑  收藏  举报