代码中的软件工程

前言

经过老师的教导,在高软课上我再一次对之前没怎么关心的代码的编译和成长过程进行了学习,同时也对于程序代码思想进行了进一步理解和深化。比如代码如何整洁高效,如何简化自己的代码,使用什么样的代码风格以及对于模块化代码的认识等。本文主要对于课上所学进行一定复现和讨论。

源代码:https://github.com/mengning/menu

参考资料: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.VSCode安装及C/C++插件安装

VScode下载地址:https://code.visualstudio.com/docs/?dv=win

下载完成后直接打开,

 

 

 先打开扩展页面,搜索c,打开如下图,安装即可。

 

 

 2.MinGW安装

方法一:下载地址:https://osdn.net/projects/mingw/releases/

 

 

 单击按钮即可。下载完之后打开,勾选下图所有选项:

 

 

 然后点击右上角栏下的apply changes即可。

 

 

方法二:但笔者在安装时报错,经查资料是网络问题。若读者也遇到这问题,可用如下离线包,安装解压,直接配置环境变量加入解压路径的bin文件夹即可。

链接:https://pan.baidu.com/s/1_1lbrC3tR9AaFQVf5oWeSA
提取码:6hq6

安装成功后如下图:

 

 

 3、VSCode中进行配置:

参考文章:https://blog.csdn.net/weixin_43738067/article/details/105475016

 

 

 

launch.json

点击调试后自动生成。

tasks.json

再次调试后生成。

 

 

 需要加入工程所需其他文件。

c_cpp_properties.json

 

 

 

{
    "configurations": [
        {
            "name": "Win32",
            "includePath": [
                "${workspaceFolder}/**",
                "D:/mingw64/include/*"
            ],
            "defines": [
                "_DEBUG",
                "UNICODE",
                "_UNICODE"
            ],
            "compilerPath": "D:/mingw64/bin/gcc.exe",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "gcc-x64"
        }
    ],
    "version": 4
}

全部配置成功后,运行如下图所示:

 

 

 至此,项目初步完成。

二、代码的艺术

1、代码风格和代码规范

 

在实际的软件项目开发过程中,仅靠个人便能完成整个项目变得不再实际,事实上每个人都有自己需要完成的部分,而各个部分又是彼此关联的,所以在完成自己工作的同时如何给合作伙伴带来更大的便利性就成了一个很重要的问题,这时,良好的代码风格和代码规范就成了解决问题的关键。

  1. 代码风格的原则 简明 易读 无二义性
  2. 在符合上述原则的前提下,什么才是好的代码风格呢,在这里我们把代码风格分为三重境界
  3. 程序块头部注释
  • 第一重境界也是最基本的就是规范整洁。符合常规语言规范,合理使用空格、空行、缩进、注释等。
  • 第二重境界要求逻辑清晰。没有冗余代码,做到路基清晰不仅要求程序员的编程能力,更重要的是提高设计能力。
  • 第三重境界是优雅。优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。

 

 

 

 

 如图,该程序块头部的注释将函数功能、编程语言、运行环境、版本时间、简单描述等一一列举出来,使使用者可以清楚的知道该代码的大致信息,方便其进行使用。

4.代码风格规范总结

  • 缩进:4个空格;
  • 行宽:<100个字符;
  • 在一个函数体内,逻辑上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行;
  • 在复杂的表达式中要用括号来表示逻辑优先级;
  • 不要把多条语句和多个变量的定义放在同一行;
  • 命名:合适的命名大大增加代码的可读性;
  • 类名、函数名、变量名等的命名一定要与程序利的函数保持一致,便于阅读理解;
  • 一般变量名、对象名等使用LowerCamel风格;
  • 类型、类、函数名等一般使用Pascal风格;
  • 函数名一般使用动词或者动宾短语,如get/set,RenderPage;
  • 注释和版权信息:注释也要使用英文不使用中文。可能编码错误。
  • 每个源文件头部应该有版权、作者、版本、描述等相关信息。
  • 不解释程序如何工作,要解释程序做什么,为什么这样做以及特别的地方。

2、模块化和接口

模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

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

                           无耦合                                                      松散耦合                                                     紧密耦合

 

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

以lab3.1及3.3为例进行分析模块化设计的优点

(1) lab3.1中,当加入一个新功能的时候,只需要为函数功能链表新加一个结点再写一个功能函数即可,无需修改主函数。

复制代码
1 static tDataNode head[] = 
2 {
3     {"help", "this is help cmd!", Help,&head[1]},
4     {"version", "menu program v1.0", NULL, &head[2]},
5     {"quit", "Quit from menu", Quit, NULL}
6     //若要添加一个功能,只需加入一个tDataNode结构的数据并写一个相应的处理函数
7 };
复制代码

(2) lab3.3中,将数据结构和对数据结构的操作与业务逻辑分离开。

 1 //linklist.h用来存放数据结构和它的操作
 2 
 3 typedef struct DataNode
 4 {
 5     char*   cmd;
 6     char*   desc;
 7     int     (*handler)();
 8     struct  DataNode *next;
 9 } tDataNode;
10 
11 /* find a cmd in the linklist and return the datanode pointer */
12 tDataNode* FindCmd(tDataNode * head, char * cmd);
13 /* show all cmd in listlist */
14 int ShowAllCmd(tDataNode * head);
15 
16 //其他的业务逻辑层代码都在menu.c中

2.2 可重用接口

  消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。软件开发者在重用已有的软件模块代码时一般会重点考虑四个关键因素:

  • 该软件模块是否有完善的文档说明
  • 该软件模块是否有完整的测试及修订记录
  • 采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作
  • 该软件模块是否能满足项目所要求的功能

   消费者重用的关键因素同时也是生产者重用的关键因素。接下来介绍一下接口的概念:接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。这在menu小程序中的一个最主要的体现就是linktable接口,在linktable.h文件中,声明了数据结构以及操作该结构的一组函数。如下:

复制代码
//linktable.h中的链表节点结构
typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode;


//menu.c中的cmd结构
typedef struct DataNode
{
    tLinkTableNode * pNext;
    char*   cmd;
    char*   desc;
    int     (*handler)(int argc, char *argv[]);
} tDataNode;


//menu.c中用于打印所有cmd的函数
/* show all cmd in listlist */
int ShowAllCmd(tLinkTable * head)
{
    tDataNode * pNode = (tDataNode*)GetLinkTableHead(head);
    while(pNode != NULL)
    {
        printf("%s - %s\n", pNode->cmd, pNode->desc);
        pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    return 0;
}
复制代码

在linktable.h中,我们可以看到,链表节点的内容非常简单,只包含了指向下一个相同结构的指针。这样定义的结构具有很强的通用性,任何需要使用到链表的结构都可以在定义结构的时候将其作为第一个元素进行声明,这样在使用用户自己的结构时,如果要用到通用的链表操作,一种做法是进行强制类型转换把它转换成指向链表节点的指针类型,再将其传给linktable.h中声明的函数。就像我们在这段代码的ShowAllCmd函数中看到的那样,GetNextLinkTableNode函数的功能是寻找当前链表节点的下一跳,属于linktable模块,这里pNode将自身传进函数时进行了强制类型转换,然后将函数返回值再转换回原类型,这就很好的体现了接口的可重用性。

三、线程安全

  如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  线程安全问题都是由全局变量及静态变量引起的。

 

 

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

  不可重入的函数一定不是线程安全的。

  一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。

  在menu中,在线程安全问题中引入了锁机制,用来避免多个线程操作有可能导致的线程安全问题。

  对linktable.c中部分代码进行分析:

      1)CreateLinkTable函数

      创建链表,该函数与其他函数不互斥,可重入且线程安全。

tLinkTable * CreateLinkTable()
{
    tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable));
    if(pLinkTable == NULL)
    {
        return NULL;
    }
    pLinkTable->pHead = NULL;
    pLinkTable->pTail = NULL;
    pLinkTable->SumOfNode = 0;
    pthread_mutex_init(&(pLinkTable->mutex), NULL);
    return pLinkTable;
}

  2)AddLinkTableNode函数

  该函数中有老师添加有加锁解锁过程,保证了读写时互不干扰,在加入节点时是线程安全的。

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

3)DeleteLinkTable函数

同样的,在删除时也有写操作,加锁保证了线程安全。

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 22:19  yqwan  阅读(197)  评论(0)    收藏  举报