代码中的软件工程

编译与调试环境

在 Windows 下用 GNU ToolChain 真的伤不起。课程 ppt 里的链接下不了,搞了一大堆无用操作,还是不行。最后(全局)科*上网用 MinGW.org 的 mingw-get 下载成功。lab5.2 有了 Makefile 之后,要 link 到 pthread,所以还要下。按理说装好 gcc,make 和 pthread 应该就好了。然而这个下载器,直接连接的话除了 gcc/g++ 都下不了,也没有搜索功能,找个包找到海枯石烂。(然后 Makefile 里也没给 $(CC_PTHREAD_FLAGS)main() 也没有返回值,宏 debug("xxxx") 也不知道在干嘛......)

MinGW Installer 要装的包如下

MinGW Installer 要装的包

MinGW Installer pthread

环境变量设置

路径里的 msys1.0in 包含了 make 在内的一票命令行工具,之前下过 GNU Make 貌似也可以。

命令行编译每个 lab 里的代码的话,大概格式如下:

> gcc menu.c linktable.c -o menu.exe -lpthread

调试的话,在 vscode 可以选择 gdb 来调试。

vscode

代码成长轨迹

这个命令行菜单小项目演示了一个代码不断成长的过程。各个版本都根据软件开发中的一些原则加入新的东西。

版本 内容变化 涉及
lab1 +hello.c +menu.c 运行测试、伪代码
lab2 -hello.c 源文件信息头、主函数框架
lab3.1 命令链表遍历代替分支语句
lab3.2 隐藏命令链表的遍历操作
lab3.3 +linklist.h +linklist.c 将命令链表操作独立成另一个模块
lab4 -linklist.h -linklist.c +linktable.h +linktable.c +test.c +testlinktable.c 把链表与命令分离、动态创建命令、可重入函数与线程安全、模块测试
lab5.1 -test.c call-back 函数
lab5.2 -testlinktable.c +Makefile 增加 Mackefile
lab7.1 +test.c +menu.h 把菜单独立成模块
lab7.2 +readme.txt 项目说明

代码分析与感悟

模块化与接口

软件工程这件事,说白了就是为了人。计算机科学和自然科学不同,它是个人造的东西。人干什么事情都是有目的,人发明的技术都是为了解决问题。为什么要有模块化?因为我没必要重新写以前写过的东西,因为我想用别人写好的我想要的东西。要重用代码,这个代码如果乱成一坨,那我还不如写个新的。所以代码要有规范才能好理解、要容易往里加东西但又不容易被破坏原本的结构……用正式的话总结出来就变成一条条的原则写在教科书上,再也没人看得懂了。这很矛盾。前人总结的经验没有经验你看不懂,你看不懂硬要用的话,就会过度设计;你自己遇到问题之后,你再去看这些,除了发出点“噢,我也跳过这个坑,你们还给他起了个名字”之类的感慨,也没啥了。

接口和抽象紧密相连。计算机本身就是从电路一层层抽象上来的,各个层次之间都要定义好接口,这样才能接得上。作为上层软件的程序员,不可能理解完所有的细节才去编程,否则日子没法过了。所以我不关心这一层层的黑盒里具体干了什么,我只关心他们提供的功能,而这些功能通过接口获取。代码里,把 linktable 变成模块以后,我只需要调用其接口函数,就能用到他的功能了。

那可重用的接口该如何设计?应该脱离依赖,低耦合。打个比方,lab5.1 的 linktable.c 的 SearchLinkTableNode 函数,他的搜索命中判断条件,为什么不硬编码到 if 里,而是给一个谓词参数传进来?因为要脱离依赖。不同的人用链表查找的事情不同,你都直接规定了查找条件,谁还能重用你这个模块?

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
{
    if(pLinkTable == NULL || Conditon == NULL)
    {
        return NULL;
    }
    tLinkTableNode * pNode = pLinkTable->pHead;
    while(pNode != pLinkTable->pTail)
    {    
        if(Conditon(pNode) == SUCCESS)
        {
            return pNode;				    
        }
        pNode = pNode->pNext;
    }
    return NULL;
}

再看 lab5.2 的 linktable.c 里的 SearchLinkTableNode,增加了一个参数。而 call-back 函数也增加了这个参数。这里的主要目的是,把 SearchCondition 里的判断参数 cmd 独立出来(他本来是全局的)。

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
    if(pLinkTable == NULL || Conditon == NULL)
    {
        return NULL;
    }
    tLinkTableNode * pNode = pLinkTable->pHead;
    while(pNode != NULL)
    {    
        if(Conditon(pNode,args) == SUCCESS)
        {
            return pNode;				    
        }
        pNode = pNode->pNext;
    }
    return NULL;
}

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

个人感觉,这样并没啥特别的改变。因为这个 Condition 函数, 他的参数是随着 SearchLinkTableNode 函数参数的改变而改变的。比起 lab5.1,SearchLinkTableNode 甚至还知道了关于判断条件更多的信息(Condition 除了 pNode 还需要 args)。要我想个解决方案的话,干脆不要 Condition 函数,把他换成类(如果在支持类机制的语言里)。也许可以这么写:

class Conditon()
{
    Condition(char *args) { ... }
    // 需要可变长参数的话?
    // Condition(char *args, int len) { ... }
    
    int evaluate(tLinkTableNode * pNode)
    {
        // 任何判断条件都可以
        if (pNode ... args)
        {
           ...
        }
    }
    
    char* args;
}

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, const Conditon* condition)
{
    if(pLinkTable == NULL || Conditon == NULL)
    {
        return NULL;
    }
    tLinkTableNode * pNode = pLinkTable->pHead;
    while(pNode != pLinkTable->pTail)
    {    
        if(conditon.evaluate(pNode) == SUCCESS)
        {
            return pNode;				    
        }
        pNode = pNode->pNext;
    }
    return NULL;
}

继承与多态

我以前貌似在某设计模式的书里看到过 “不要 if-else 要类” 的说法。如下代码片段所示,通过给所有的命令套进一个 Node 里,Node 里封装一个统一的函数指针 int (*handler)(),这样就可以通过这个统一的指针,调用到不同的函数。再看下下面,LinkTableNode 作为一个链表结点,他逻辑上只需要能指向下一个结点就行了,至于他里面放什么数据,不是他非得关心的问题。所以把他作为父类,不同的结点继承他。这些不同的结点作为子类,放不同的具体数据。

// lab2/menu.c
 while(1)
    {
        scanf("%s", cmd);
        if(strcmp(cmd, "help") == 0)
        {
            printf("This is help cmd!
");
        }
        else if(strcmp(cmd, "quit") == 0)
        {
            exit(0);
        }
        else
        {
            printf("Wrong cmd!
");
        }
    }

// lab3.1/menu.c
    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
", p->cmd, p->desc);
                if(p->handler != NULL)
                {
                    p->handler();
                }
                break;
            }
            p = p->next;
        }
        if(p == NULL) 
        {
            printf("This is a wrong cmd!
 ");
        }
    }
// lab4
// linktable.c
typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode

// menu.c
typedef struct DataNode
{
    tLinkTableNode * pNext;
    char*   cmd;
    char*   desc;
    int     (*handler)();
} tDataNode

可重入函数与线程安全

若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入的(reentrant)。可重入和线程安全密切相关。可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。操作系统说,信号量用来保护临界资源,而临界资源是一段可共享的代码。在 linktable.c 里可以看到很多类似的代码:

       
        pthread_mutex_lock(&(pLinkTable->mutex));
        pLinkTable->pHead = pLinkTable->pHead->pNext;
        pLinkTable->SumOfNode -= 1 ;
        pthread_mutex_unlock(&(pLinkTable->mutex));

// 简化一下

        P(mutex)
        // do something...
        V(mutex)
            

信号量的初始化和销毁:

   
   pthread_mutex_init(&(pLinkTable->mutex), NULL);
   // ...
   pthread_mutex_destroy(&(pLinkTable->mutex))
       

工程化

当程序员多了起来,代码多了起来,工程复杂了起来,就必须要使用一定的方法来管理项目了。c/c++ 项目的构建系统,在 Linux / Unix 下有 make,Windows 下 MSVC 的 proj 和 sln,跨平台的 CMake。这些工具是通过编写一些规定好语法的配置文件,来指导编译链接甚至安装的过程(说白了就是把一行行敲的命令和编译链接选项,写进了文件里)。项目里展示的 make 工具的 Makefile :

#
# Makefile for Menu Program
#

CC_PTHREAD_FLAGS			 = -lpthread
CC_FLAGS                     = -c 
CC_OUTPUT_FLAGS				 = -o
CC                           = gcc
RM                           = rm
RM_FLAGS                     = -f

TARGET  =   test
OBJS    =   linktable.o  menu.o test.o

all:	$(OBJS)
	$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) $(CC_PTHREAD_FLAGS)
	
# CC_PTHREAD_FLAGS 不要忘记加进来

.c.o:
	$(CC) $(CC_FLAGS) $<

clean:
	$(RM) $(RM_FLAGS)  $(OBJS) $(TARGET) *.bak

Makefile有三个非常有用的变量。分别是(@、)^和$<,代表的意义分别是: (@ 表示目标文件;)^ 表示所有的依赖文件;(< 表示第一个依赖文件。 目标.c.o和)<结合起来是精简写法,表示要生成一个.o目标文件就自动使用对应的.c文件来编译生成。

项目里展示的工程化的流程还有,开发前先测试环境啦(helloworld)、写完一个模块先写测试试一下啦(testlinktable)、增加 readme 啦……总的来说,还是值得学习的一个小项目。

参考

[1] https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程

原文地址:https://www.cnblogs.com/tandandan/p/13896466.html