1. 程式人生 > 實用技巧 >見微知著:Menu小程式中的程式碼藝術

見微知著:Menu小程式中的程式碼藝術

  筆者之前一直使用Vistual Studio2019作為開發工具,而將VS Code一直簡單的作為編輯器使用,並未深入瞭解VS Code。正好藉著孟老師的軟體工程課程深入學習一下如何將VS Code+gcc工具集作為開發環境。

  本文下述內容大致分為幾部分:

  1. 編譯與環境配置
  2. Menu小程式的簡要分析
  3. Menu小程式引發的一系列問題的思考

一、編譯與環境配置

  VsCode最基本的功能是作為編輯器,他並沒有編譯功能,因此我們下載好VS Code之後還需要下載編譯器。

  編譯器我選擇了MinGW,但是要注意,因為MinGW下載為外網環境,因為網路原因很有可能下載失敗,建議直接下載離線檔案。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  這裡筆者提供離線檔案的網盤地址,有需要的童鞋可以自行下載

  連結:https://pan.baidu.com/s/1n3gZBlvuF2HIllhjzRM3qA  提取碼:ch8u

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  如選擇使用提供的mingw-get-setup.exe安裝,選擇好需要安裝的工具,一直next即可

  安裝好了MinGW之後,我們需要配置環境變數才可使用gcc

  環境變數配置流程很簡單,向PATH下新增MinGW資料夾下的bin資料夾路徑即可

  安裝好了MinGW並配置了環境變數之後,我們可以通過在cmd命令列執行

gcc -v

測試是否安裝成功(安裝成功如下圖所示)

  此時我們VS Code與編譯器均已安裝完成,基本環境搭建已完成,我們馬上大功告成,要想在VS Code上成功的執行除錯程式,我們還需進行配置檔案的修改,此時我們以孟老師給出的src資料夾下的hello.c為例,我們要想執行hello.c,需要更改.vscode下的launch.json與c_cpp_properties.json檔案

具體修改細節如下:

1 //c_cpp_properties.json檔案修改處
2 "miDebuggerPath": "D:\\MinGW\\mingw64\\bin\\gdb.exe" 3 //launch.json檔案修改gcc的路徑 4 "compilerPath": "D:\\MinGW\\mingw64\\bin\\gcc.exe",

現在我們環境配置好,大功告成,通過gcc編譯執行如下

如果我們嫌棄每次輸入命令列編譯很麻煩,這時候VS Code的一個優勢便顯示出來即豐富的外掛,我們去安裝如下外掛

  

安裝外掛成功後,在使用者介面右上角會提供一個執行按鈕,點選執行按鈕,VS Code便會自動執行指令碼自動進行檔案的編譯與執行,如下圖所示,至此,我們的環境配置大功告成。

二、Menu小程式的簡要分析

  src資料夾內,共有十個lab資料夾,一步步描述了Menu小程式的程式碼構建過程,下面我簡要分析一下每一步修改的原因即體現的軟體工程思想(注,為了方便分析與檢視,下面每增加一個.h檔案,便將該檔案介面程式碼展示一下)

  1.lab1

  lab1下包含 hello.c 檔案與 menu.c 檔案,hello.c 檔案用於測試環境是否搭建完成,menu.c 檔案是本程式的框架程式碼(此處為虛擬碼)

  2.lab2

  lab2下移除了 hello.c 檔案,與此同時完善了 menu.c 檔案的虛擬碼,使其成為了可以執行的框架程式碼

  3.lab3.1

  lab3.1下將可能重複用到的程式碼段封裝到函式內部(分別封裝了 Help() 函式和Quit() 函式),同時引入了結構體連結串列,將指令,指令詳細描述,指令對應函式存入一個結構體內,初步體現了可重用介面與模組化的思想

  4.lab3.2

  lab3.2將模組化與可重用介面的思想進一步體現,進一步將主函式內遍歷結構體連結串列尋找cmd的操作封裝到了函式 FindCmd() 內,同時設計了一個新函式 FindCmd() 來為使用者提供使用幫助,這很大程度上提高了程式碼的可用性,使用者可以通過 Help 獲取相關幫助,提高了使用者的體驗感

  5.lab3.3

  lab3.3程式碼部分並沒有改變,但是將結構體程式碼獨立到一個檔案 linklist.h 內,這極大程度的體現了模組化的思想,同時也降低了碼農的閱讀與修改程式碼的難度,程式碼的邏輯層次更加明顯。同時還有一個細節,在 linklist.h 檔案內進行函式介面的宣告,而實現的細節儲存到 linklist.c 檔案內,這極大程度上提高了閱讀效率。

  linklish.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);

  6.lab4

  lab4改變較大,增加了 Menu 資料的初始化函式 InitMenuData() ,可以動態建立Menu,增加了資料的靈活性,同時新引入了新的結構體 LinkTable 並增加了執行緒安全的互斥量 mutex,LinkTable 比之前的 linklist 增加了更多的內容,可以很方便的增刪查節點。LinkTable更進一層次的實現了模組化,內聚程度更進一步,將業務層與底層節點分離,與menu模組之間的耦合度降低。

  linktable.h介面如下所示

/*
 * LinkTable Node Type
 */
typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode;

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

/*
 * Create a LinkTable
 */
tLinkTable * CreateLinkTable();
/*
 * Delete a LinkTable
 */
int DeleteLinkTable(tLinkTable *pLinkTable);
/*
 * Add a LinkTableNode to LinkTable
 */
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
/*
 * Delete a LinkTableNode from LinkTable
 */
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
/*
 * get LinkTableHead
 */
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
 * get next LinkTableNode
 */
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);

#endif /* _LINK_TABLE_H_ */

  本部分還引入了執行緒安全的概念,通過互斥量 mutex 為執行緒上鎖與解鎖,解決不同執行緒之間的記憶體安全問題。執行緒安全本質上其實是記憶體的安全!

  例如,上述程式碼通過對mutex進行上鎖與解鎖,實現了 SumOfNode 變數記憶體的安全,當不同執行緒訪問該變數時,若對該變數不加鎖,便可能會發生讀寫衝突,造成記憶體儲存資料的正確性得不到保障。

  7.lab5.1

  lab5.1引入了回撥函式 callback,因筆者對callback函式之前瞭解不多,所以著重記錄一下該方法

intSearchCondition(tLinkTableNode*pLinkTableNode) { tNode*pNode=(tNode*)pLinkTableNode; if(pNode->data==6) { returnSUCCESS; } returnFAILURE; }
------------------------------------分割線-----------------------------------------
 /*searchbycallback*/ debug("SearchLinkTableNode\n"); tNode*pTempNode=(tNode*)SearchLinkTableNode(pLinkTable,SearchCondition);//SearchCondition()函式定義上面給出 printf("%d\n",pTempNode->data);

引用維基百科上對回撥的定義:

  In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback.

  把一段可執行的程式碼像引數傳遞那樣傳給其他程式碼,而這段程式碼會在某個時刻被呼叫執行,這就叫做回撥。如果程式碼立即被執行就稱為同步回撥,如果在之後晚點的某個時間再執行,則稱之為非同步回撥。

打個比方,有一家旅館提供叫醒服務,但是要求旅客自己決定叫醒的方法。可以是打客房電話,也可以是派服務員去敲門,睡得死怕耽誤事的,還可以要求往自己頭上澆盆水。這裡,“叫醒”這個行為是旅館提供的,相當於庫函式,但是叫醒的方式是由旅客決定並告訴旅館的,也就是回撥函式。而旅客告訴旅館怎麼叫醒自己的動作,也就是把回撥函式傳入庫函式的動作,稱為登記回撥函式(to register a callback function)

  回撥機制提供了非常大的靈活性,這種靈活性是怎麼實現的呢?乍看起來,回撥似乎只是函式間的呼叫,但仔細一琢磨,可以發現兩者之間的一個關鍵的不同:在回撥中,我們利用某種方式,把回撥函式像引數一樣傳入中間函式。可以這麼理解,在傳入一個回撥函式之前,中間函式是不完整的。換句話說,程式可以在執行時,通過登記不同的回撥函式,來決定、改變中間函式的行為。

  8.lab5.2

  lab5.2 改進了函式SearchLinkTableNode()函式

//lab5.1
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))

//lab5.2
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)

  lab5.1記憶體在問題,底層程式碼反而需要依賴業務層程式碼回撥引數,lab5.2內進行函式改造,將所需資訊作為函式傳入,降低耦合度為鬆散耦合。

  lab5.2 同時還增加了Makefile檔案,使用make命令來構建工程,會使原本複雜繁瑣的工作簡化許多,極大地提高了效率,程式碼如下,

  一個工程中的原始檔不計其數,其按型別、功能、模組分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些檔案需要先編譯,哪些檔案需要後編譯,哪些檔案需要重新編譯,甚至於進行更復雜的功能操作,因為 makefile就像一個Shell指令碼一樣,其中也可以執行作業系統的命令.

#
# Makefile for Menu Program
#

CC_PTHREAD_FLAGS	     = -lpthread //給右側gcc等提供別名
CC_FLAGS                     = -c 
CC_OUTPUT_FLAGS	             = -o
CC                           = gcc
RM                           = rm
RM_FLAGS                     = -f

TARGET  =   menu
OBJS    =   linktable.o  menu.o

all:	$(OBJS)
	$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS)

.c.o:
	$(CC) $(CC_FLAGS) $<  //$<為自動變數

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

  9.lab7.1

  在lab7.1中,將分層設計思想進一步發揮,將 menu 層與 linktable 層一樣,徹底從 main()函式內分離出來,實現了進一步的解耦合與模組化。

  在本lab中,新增 MenuConfig() 函式與 ExecuteMenu() 函式,將 menu 層完全提取出來,並在menu.h檔案內向外部提供相關 menu 功能的介面,menu.h內容如下所示

/* add cmd to menu */
int MenuConfig(char * cmd, char * desc, int (*handler)());

/* Menu Engine Execute */
int ExecuteMenu();

  至此,徹底將 Menu 層,LinkTable 層徹底與main()函式分割開來,程式設計師只需在 main() 函式內呼叫相關介面便可實現Menu的初始化,修改與執行。程式設計師此時便徹底將底層封裝了起來,現在只需要關注底層對外提供的介面便可實現對功能的修改。

  10.lab7.2

  lab7.2較lab7.1程式碼方面並無改動,而是增加了 readme.txt 檔案,為使用者提供了使用指南,提供了該專案的Build流程與操作指南。

  至此,本專案搭建完畢,雖然 menu 專案並不算太大,但其中包含的軟體工程的思想卻體現的淋漓盡致,麻雀雖小,五臟俱全。模組化設計,可重用介面,執行緒安全,使用者體驗度等等在該專案中均有所體現,下面筆者就這些方面的談一下自身的理解與總結。

三、Menu小程式引發的一系列問題的思考

  1.模組化程式設計

  模組化設計與可重用介面有著千絲萬縷的聯絡,這都是模組化程式設計的內容,將程式的不同功能分割為獨立的,可互換的模組。

  目標是為了實現高內聚,低耦合的目標

  將程式中不同功能分為不同的模組,一方面邏輯結構更加清晰,另一方面便於分工。不同的模組之間要協同合作才可以實現最終的功能,而不同模組之間是如何協同的呢,這就引入了介面的概念。介面:通常是說模組或元件留給外界使用的方式,稱作介面,即你在使用這些模組或框架或者其他什麼的時候,你所真正使用的東西就是API介面,另外介面在特定的程式語言中有特定的含義和語法要求。

  我認為模組化設計主要目標是使得邏輯結構更加簡單,將不同的功能模組分別實現,對外部僅僅提供API介面,隱藏了底部程式碼的實現細節,不僅保證了程式碼的安全性(防止不懂的使用者隨意修改而造成錯誤),而且也方便了程式設計師的呼叫,同時出現問題的時候,也方便了程式設計師進行Debug。還有一層次要的目標是為了方便分工,現如今軟體的體量越來越大,許多工程不太可能靠一個人的力量完成,需要很多人協同合作,這樣將不同的功能模組分給不同的程式設計師,其他程式設計師只要知道其他功能的介面便可呼叫該功能。

  而可重用介面我認為主要目標是不要重複造輪子,真正的程式碼工程是追求效率的,很多功能既然已經實現了,若我們為了追求效率,當然希望直接呼叫他人已經實現的功能,這時,介面的作用便體現出來,當然了,若是我們為了學習,增強自己的程式碼能力,還是建議自己親手實現以下相關功能。

  menu小程式中,孟老師便是將模組化設計與可重用介面很好的體現出來,我們將該程式分為了三部分

  •   linktable

  •   menu

  •   主函式

  在 linktable.h 內提供了對 linktable 資料結構進行建立,增加,刪除,查詢操作的介面(函式宣告在上面已經給出)

  在 menu.h 內提供了對menu選單進行配置與執行功能的介面

  而程式設計師只需要在主函式內呼叫上面兩個 .h 檔案給出的介面,便可實現menu的所有功能。每個不同的模組內聚程度都很高,不同模組之間的耦合儘可能的降到了最低,不會因為單個模組出現問題,而導致其他模組出現問題。

   2.執行緒安全

    執行緒安全本質上就是記憶體的安全,

    不可重入函式執行緒不安全,執行緒安全不一定是可重入函式,而可重入函式一定是執行緒安全的

    確保執行緒安全最基本的操作時加鎖,當對一個共享變數進行操作時,對其加鎖,確保其他執行緒不會同時修改該變數,當操作完畢後進行解鎖。

    本例中,執行緒安全就是通過加鎖的方式。

struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int			SumOfNode;
    pthread_mutex_t mutex;//確保執行緒安全的鎖

};

//加鎖與解鎖示例
pthread_mutex_lock(&(pLinkTable->mutex));//對變數SumOfNode進行加鎖
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));//對變數進行解鎖    

  加鎖屬於互斥同步的一種, 互斥同步是最常見的一種併發正確性保障手段。同步是指在多執行緒併發訪問共享資料時,保證共享資料在同一時刻只被一個執行緒使用(同一時刻,只有一個執行緒在操作共享資料)。而互斥是實現同步的一種手段,臨界區、互斥量和訊號量都是主要的互斥實現方式。因此,在這4個字裡面,互斥是因,同步是果;互斥是方法,同步是目的。

  現在隨著硬體指令集的發展,出現了基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭用,產生了衝突,那就再採用其他的補償措施。(最常見的補償錯誤就是不斷地重試,直到成功為止),這種樂觀的併發策略的許多實現都不需要把執行緒掛起,因此這種同步操作稱為非阻塞同步。

  我們在書寫程式碼時,一定要注意執行緒安全問題,因為有些時候出現執行緒不安全的情況後,很長一段時間不會再出現類似情況,這種情況下程式設計師debug會十分痛苦... 因此要格外注意

  這篇隨筆到這就結束了,通過對孟老師 menu 小程式的分析,我也領悟了很多軟體工程中的重要思想,在此也做了簡要的總結與分析,若有錯誤,歡迎大家指出,最後感謝教授這門課的孟寧老師的教誨。