1. 程式人生 > 實用技巧 >Codeforces 235C Cyclical Quest 字尾自動機

Codeforces 235C Cyclical Quest 字尾自動機

前言

首先感謝孟寧老師的教學指導,通過課上的學習,我瞭解到了許多軟體工程的設計思想。

孟老師通過一個簡單的menu小程式,直觀細緻地給我們講解了程式碼規範、模組化設計、可重用介面以及執行緒安全等問題,我從中學到了很多。

本文中用到的menu程式原始碼:

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

環境搭建

首先必須確保gcc已經安裝好,在mac平臺下用brew install gcc

即可安裝,安裝完成後檢視gcc版本:

然後在vscode中安裝C/C++外掛

執行menu程式碼:

  • 在專案目錄下用make進行編譯,然後./test執行test案例,如下圖所示即為執行成功

  • 對各功能進行測試

程式碼測試正常

程式碼風格規範

以前我總是認為程式碼是寫給機器看的,完全不注意程式碼的規範,這樣的程式碼寫出來,往往會讓日後的維護變得非常的困難,連自己都不知道寫的是什麼含義,孟老師用menu程式裡的程式碼給我做了個示範,讓我深切明白了程式碼規範性的重要,程式碼不只是寫給機器看的,更是寫給人看的。

註釋規範

/**************************************************************************************************/
/* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015                                                  */
/*                                                                                                */
/*  FILE NAME             :  menu.c                                                               */
/*  PRINCIPAL AUTHOR      :  Mengning                                                             */
/*  SUBSYSTEM NAME        :  menu                                                                 */
/*  MODULE NAME           :  menu                                                                 */
/*  LANGUAGE              :  C                                                                    */
/*  TARGET ENVIRONMENT    :  ANY                                                                  */
/*  DATE OF FIRST RELEASE :  2014/08/31                                                           */
/*  DESCRIPTION           :  This is a menu program                                               */
/**************************************************************************************************/

/*
 * Revision log:
 *
 * Created by Mengning, 2014/08/31
 *
 */

以這段menu.c中的頭部註釋為例:註釋要使用英文,不要使用中文或特殊字元,要保持原始碼是ASCII字元格式檔案,要解釋程式做什麼,為什麼這麼做,以及特別需要注意的地方,每個原始檔頭部應該有版權、作者、版本、描述等相關資訊。

命名規範

  • 類名、函式名、變數名等的命名一定要與程式裡的含義保持一致,以便於閱讀理解;
  • 型別的成員變數通常用m_或者_來做字首以示區別;
  • 一般變數名、物件名等使用LowerCamel風格,即第一個單詞首字母小寫,之後的單詞都首字母大寫,第一個單詞一般都表示變數型別,比如int型變數iCounter;
  • 型別、類、函式名等一般都用Pascal風格,即所有單詞首字母大寫;
  • 型別、類、變數一般用名詞或者組合名詞,如Member
  • 函式名一般使用動詞或者動賓短語,如get/set,RenderPage;
typedef struct DataNode
{
    tLinkTableNode * pNext;
    char*   cmd;
    char*   desc;
    int     (*handler)(int argc, char *argv[]);
} tDataNode;

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

如menu.c中的這段程式碼為例:變數名為駝峰式,函式名、型別名首字母大寫,並且變數名的含義與程式含義一致,讀起來一目瞭然

模組化設計

簡介

模組化是在軟體系統設計時保持系統內各部分相對獨立,以便每一個部分可以被獨立地進行設計和開發。這個做法背後的基本原理是關注點的分離,關注點的分離的思想背後的根源是由於人腦處理複雜問題時容易出錯,把複雜問題分解成一個個簡單問題,從而減少出錯的情形。模組化軟體設計最終每一個軟體模組都將只有一個單一的功能目標,並相對獨立於其他軟體模組,使得每一個軟體模組都容易理解容易開發。

模組化程度的一個重要指標就是耦合度,耦合度是指軟體模組之間的依賴程度,一般可以分為緊密耦合、鬆散耦合和無耦合。而內聚度是指一個軟體模組內部各種元素之間互相依賴的緊密程度。

如何來具體實現模組化呢,就是將資料結構和它的操作與選單業務處理進行分離處理,儘管還是在同一個原始碼檔案中,但是已經在邏輯上做了切分,可以認為有了初步的模組化。

模組化設計的實現

還是以menu程式的程式碼為例,可以看出其分為三個模組:

  1. 程式的入口test

    以下為test.c中的main函式內容,可以看出其主要作用是提供程式的入口,與使用者打交道

int main()
{
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("time","Show System Time",Time);
    MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
    ExecuteMenu();
}
  1. 選單邏輯menu

    menu.c中存放的是test.c背後邏輯的具體實現,將實現與呼叫相分離,便於程式碼的維護

/* add cmd to menu */
int MenuConfig(char * cmd, char * desc, int (*handler)())
{
    tDataNode* pNode = NULL;
    if ( head == NULL)
    {
        head = CreateLinkTable();
        pNode = (tDataNode*)malloc(sizeof(tDataNode));
        pNode->cmd = "help";
        pNode->desc = "Menu List";
        pNode->handler = Help;
        AddLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    pNode = (tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd = cmd;
    pNode->desc = desc;
    pNode->handler = handler; 
    AddLinkTableNode(head,(tLinkTableNode *)pNode);
    return 0; 
}
  1. 選單使用到的資料結構linktable

    如下為Linktable.c中的程式碼,包括了結點的定義,以及對連結串列的建立,插入,刪除等操作

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

};
/*
 * Create a LinkTable
 */
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;
}

可重用介面

簡介

介面就是互相聯絡的雙方共同遵守的一種協議規範,在我們軟體系統內部一般的介面方式是通過定義一組API函式來約定軟體模組之間的溝通方式。

軟體設計中的模組化程度便成為了軟體設計有多好的一個重要指標,一般我們使用耦合度(Coupling)和內聚度(Cohesion)來衡量軟體模組化的程度,耦合度是指軟體模組之間的依賴程度,一般可以分為緊密耦合(Tightly Coupled)、鬆散耦合(Loosely Coupled)和無耦合(Uncoupled)。一般在軟體設計中我們追求鬆散耦合,因為這樣的話介面就可以重複使用了。

要想使介面可重用,就得使介面通用化,通用介面定義的基本方法:引數化上下文, 移除前置條件,簡化後置條件。

如何實現

具體還是以menu程式為例,看看是如何實現可重用介面的:

在linktable.h中可以看到一個新的資料結構,該結構體中只保留了最基本的遍歷功能,具體的data資料並沒有包含,這是因為使用者可以自己新增自己所需要的資料,而linktable.h這個通用介面只需要實現最基本的遍歷功能即可,無需關心資料,只需關心遍歷這一個邏輯,這樣就使介面更通用,可重用性更高。

typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode;

尋找某個節點時使用FindCmd方法,資料結構裡的的尋找節點方法又通過SearchCondition來判斷此節點是否是我們所找的節點,而SearchCondition用依賴cmd這個業務層中定義的變數,所以linktable中依然存在業務層的痕跡,所以需要修改。

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

/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
    return  (tDataNode*)SearchLinkTableNode(head,SearchCondition);
}

為了更加通用,可以修改cmd陣列,使其變為區域性變數,同時增加一個args引數

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), 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;           
}

執行緒安全

簡介

執行緒是作業系統最小的執行單位,在一個程序中可能共享同一個全域性變數。在一個併發的計算機中,同時有兩個或以上的執行緒在執行,如果每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行讀寫操作,一般都需要考慮執行緒同步,否則就可能影響執行緒安全。所以比較合適的方法就是為這個變數上讀寫鎖。

如何實現

以Linktable.c中的這段程式碼為例

  • 在定義結構體時便定義了執行緒互斥量
typedef struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int            SumOfNode;
    pthread_mutex_t mutex;
}tLinkTable;
  • 在結點的插入操作中加入加鎖與解鎖的操作,從而實現了執行緒安全
/*
 * Add a LinkTableNode to LinkTable
 */
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;
}