位元詩人 [C 語言] 程式設計教學:善用開發工具改善 C 程式專案

C 語言開發工具
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

除了編譯器和編輯器等必要的軟體外,還有許多和撰寫 C 程式相關的開發工具。由於這些軟體不是必備的,所以一些初階的 C 語言教材不會介紹這些軟體。但這些工具對於撰寫 C 專案有不同面向的助益,行有餘力的話,可以試著關注一下這些工具,慢慢學習這些工具的使用方式。

試著用多種 C 編譯器編譯 C 程式碼

在大部分情形下,我們把 C 編譯器當成老師,根據 C 編譯器所吐出的訊息來修正我們的程式碼。但在很偶然的情形下,C 編譯器也會出錯。

筆者曾經在某個知名的類 Unix 系統上用 GCC 和 Clang 編譯同一份 C 專案,GCC 編譯出來的執行檔有誤,但 Clang 編譯出來的結果是正常的。事後用 Valgrind 和其他類 Unix 系統的 GCC 確認過,不是 C 專案本身的問題。

所以,最好用 GNU Make 或 CMake 這類跨平台軟體設置 C 專案,將專案設置成可應對多種 C 編譯器,這時候就可以交叉使用不同 C 編譯器來編譯同一份 C 專案。一般來說,專案最好能夠對應 GCC、Clang、Visual C++ 等主流 C 編譯器。詳見下節。

編譯自動化軟體 (Build Automation)

剛開始學 C 語言時,我們會直接用 IDE 來管理專案,這樣做的好處在於不需要手動寫設定檔,可以專心在學習語法上。但是,過度依賴 IDE 的專案管理功能,會使得專案的可攜性降低。IDE 並不是 C 專案必要的部分,我們可以使用完全不依賴 IDE 的編譯自動化軟體來管理管案,讓專案和 IDE 脫勾。

C 語言本身沒有官方的專案格式,但我們可利用一些社群方案來管理 C 專案。常見的編譯自動化軟體如下:

Make 算是編譯自動化軟體的鼻祖,也是 POSIX 標準的項目之一,在類 Unix 系統上相當普遍。但 Make 算是純命令列工具,無法善用 Visual Studio 等 IDE 的專案管理功能。其實我們可以用 Make 直接在命令列操作 Visual C++,只是實務上會這樣做的程式人甚少。

由於寫跨平台的 Makefile 相當困難,因而出現 Autotools 這類 Makefile 生成器。Autotools 會根據系統當時的情境和使用者所設置的參數,生成相對應的 Makefile。但 Autotools 是設計給類 Unix 系統使用的,Windows 上則無法使用。

相較於 Make,CMake 不是真正的編譯自動化軟體,而是編譯自動化設定檔生成器。透過 CMake,我們可以從單一設置產生 Make、Visual Studio、Xcode、Code::Blocks 等多種軟體的編譯自動化設定檔,再分別利用這些軟體去編譯 C 專案,利用這種機制來間接達成跨平台的特性。

Visual Studio 和 CLions 等商業 IDE 都支援 CMake,代表 CMake 的確是受到重視的軟體專案。由於 CMake 可以涵蓋 Make 的功能,但反之則否,我們應優先學習 CMake。

筆者先前寫了一些關於 GNU Make 的文章,有興趣的讀者可以參考一下。

除錯器 (Debugger)

程式往往不會一次就寫對,在程式發生錯誤時,就要對程式除錯。除錯器 (debugger) 就是用來輔助除錯的軟體。除錯器會在程式執行到中斷點 (breaking point) 時將程式中止,這時候程式設計者就可以觀察程式內部狀態。觀察狀態的方式是觀察變數執行到中斷點時的值。

對於 C 語言來說,GDB 是相當常見的除錯器。若要使用 GDB 除錯,在編譯程式時要加上 -g 選項:

$ gcc -g -o program file.c

接著,用 gdb 程式去執行該程式:

$ gdb ./program

這時候會來到 GDB 的互動式環境:

(gdb)

輸入 list 指令加上行數可以跳到特定行數所在的位置:

(gdb) list 35

輸入 break 指令加上行數可以在特定行數設置中斷點:

(gdb) break 34

輸入 run 指令會啟動程式至中斷點所在的位置:

(gdb) run

輸入 print 指令並指定變數即可讀出該變數在程式執行到中斷點時的狀態:

(gdb) print out

這裡的 out 是變數名稱,而非指令的一部分,請讀者不要死記這個變數名稱。

確認完後,用 clear 清除中斷點:

(gdb) clear

執行 quit 即可離開 GDB。

(gdb) quit

有些程式設計者完全不用除錯器除錯,而用 printf 等函式在終端機中印出資料來觀察程式狀態。但使用 printf 函式印出資料的方式,無法將程式中止在特定步驟,和使用除錯器仍有差異。此外,使用 printf 函式會在程式中留下額外的程式碼,而使用除錯器不會。

GDB 本身是命令列工具,而有些程式設計者不習慣在命令列環境下除錯。許多 IDE 都內建除錯器的功能,就可以在圖形介面環境下除錯。不論是使用 GDB 或其他除錯器,最好還是花一點時間學習除錯器的用法。

自動程式碼重排 (Code Formatting)

我們在撰寫 C 程式碼時,通常會把 C 程式碼排列整齊。排列後的 C 程式碼不僅易於閱讀,之後要修改也比較容易。但排列程式碼是相當機械化的動作。對於多人協作的專案來說,要保持一致的程式碼風格更是困難。利用自動程式碼重排軟體,我們可以省下排列程式碼的時間和心力,輕鬆取得較一致的撰碼風格。

常見的 C 語言自動程式碼重排工具如下:

indent 為例,使用方式如下:

$ indent file.c

這時候會產生排列過的 file.c 和備分檔 file.c.~ 。如果對排列的結果不滿意,可以及時用備分檔回復。

由於 indent 是 GNU 專案,預設使用 GNU 風格。但台灣的 C 語言教材甚少使用 GNU 風格,較常用 K&R 風格。可以用以下參數來更動排版風格:

$ indent -kr file.c

其實 indent 的選項很多,為了節省程式人的時間,會用前述的風格 (style) 快速設置一系列選項。常見的風格如下:

  • -gnu:GNU 專案所用的風格
  • -kr:K&R 風格,台灣的 C 語言教材常用
  • -orig:BSD 風格

不論使用那套程式碼重排軟體,在多人協作時,建議先配置好共通的設定檔,在團隊中皆使用該設定檔,就可以取得一致的撰碼風格。

靜態程式碼檢查 (Static Code Analysis)

由於 C 語言是靜態型別語言,本來就可以比動態型別語言抓出更多錯誤。此外,GCC 和 Clang 等編譯器也內建許多非錯誤的警告訊息,可透過參數開啟這些資訊。不過,仍然有一些給 C 或 C++ 使用的靜態程式碼檢查軟體。以下是常見的方案:

註:目前 Splint 已經停止維護,故不建議繼續使用。

以下是使用 cppcheck 檢查 C 程式碼的範例指令:

$ cppcheck --enable=warning,style,performance,portability --std=c89 path/to/file.c

在此指令中,cppcheck 會檢查四個項目,並檢查 C 程式碼是否符合 ANSI C (C89)。

以下是使用 infer 檢查 C 專案的範例指令:

$ infer run -- make

這時候 infer 會在編譯專案的過程中掃描編譯的 C 程式碼,並回報有問題的程式碼。若要再次進行檢查,得先清除專案的暫存物件 (.o)。

由於靜態程式碼檢查軟體會有偽陽性,這些軟體檢查的結果,無法取代我們自己對程式碼的了解。如果對這些軟體吐出的訊息有疑問,還是得自己去查詢相關的資訊。

記憶體用量檢查 (Memory Checking)

C 語言在預設情境下需要手動管理記憶體,記憶體處理不當就成了 bug 的來源之一。所幸,我們可以使用記憶體管理軟體來檢查我們寫的 C 程式碼是否有錯。

在類 Unix 系統上最知名的記憶體檢查軟體就是 Valgrind。由於 Valgrind 已經問世多年,算是相當成熟的軟體。當 Valgrind 報錯時,九成以上是程式碼本身的問題,只有一成以內是 Valgrind 誤判。

假定我們的程式為 program,搭配 Valgrind 的指令如下:

$ valgrind ./program

基本上就是把要檢查的程式當成 Valgrind 軟體的第一個參數即可。如果執行目標程式時要加參數,也可以直接加在第二個之後的參數。

如果沒有記憶體洩露,Valgrind 會出現類似以下的報告:

==5030== HEAP SUMMARY:
==5030==     in use at exit: 0 bytes in 0 blocks
==5030==   total heap usage: 2,071 allocs, 2,071 frees, 56,470 bytes allocated
==5030== 
==5030== All heap blocks were freed -- no leaks are possible

如果出現記憶體洩露,Valgrind 則會出現類似以下的報告:

==5295== HEAP SUMMARY:
==5295==     in use at exit: 24 bytes in 1 blocks
==5295==   total heap usage: 2,071 allocs, 2,070 frees, 56,470 bytes allocated
==5295== 
==5295== LEAK SUMMARY:
==5295==    definitely lost: 24 bytes in 1 blocks
==5295==    indirectly lost: 0 bytes in 0 blocks
==5295==      possibly lost: 0 bytes in 0 blocks
==5295==    still reachable: 0 bytes in 0 blocks
==5295==         suppressed: 0 bytes in 0 blocks
==5295== Rerun with --leak-check=full to see details of leaked memory

Valgrind 會吐出程式執行的堆疊,讓我們知道記憶體洩露的發生位置,但仍然要程式人自己去檢查程式碼,從中找出錯誤。

Windows 和 macOS 無法使用 Valgrind,得使用其他替代軟體,像是 Dr. Memory

如果知道自己的程式有用到手動記憶體配置,最好花一下時間用 Valgrind 等軟體檢查一下,並自行修復錯誤。透過這樣的過程,養成自己良好的撰碼習慣。

其實 C 語言有第三方 GC (垃圾回收器) 函式庫,像是 Boehm GC。但真正會使用 Boehm GC 或其他 GC 函式庫的 C 專案很少。此外,現在已有 Golang 或 Rust 等自動管理記憶體的編譯語言,使用起來更加簡單。現行的主流手法仍是好好地用 Valgrind 等軟體檢查自己的 C 程式碼是否有記憶體使用的問題。

測試程式 (Testing)

測試程式是用來檢查主程式是否有問題的程式,不會隨主程式一起發佈出去,只是在開發時期做為內部使用。初學時程式規模很小,往往會過度依賴手動檢查,忽略自動化測試所帶來的便利性,養成不良的習慣。

寫測試程式是一個先苦後甘的過程。因為寫測試程式不會直接增加程式人的產能,算是額外做的工。但我們日後要重構 (refactoring) 專案時,若專案當時有留下測試程式,可讓重構的過程更加順暢。

以下是一些常見的 C 語言測試框架:

如果不想用額外的函式庫,也可以自己用 C 內建的控制結構寫一些簡單的測試程式。

撰寫程式文件 (Documentation)

如果專案是要對外發佈的,除了寫程式碼外,也要幫專案寫文件。外部專案使用者不會在缺乏足夠的文件、未了解專案如何使用時,自動自發去閱讀專案的程式碼。使用者只會在喜歡或信任該專案,但想進一步了解該專案的實作細節時,才會願意花時間去閱讀專案程式碼。

以下是兩種不同的專案文件撰寫工具:

  • Doxygen:撰寫軟體 API 文件
  • Sphinx:撰寫軟體說明文件

API 文件如同軟體專案的指引,會詳細地展示每個函式或物件的參數、回傳值等資訊。但不一定會有範例程式。API 文件是對專案已有一定熟悉度,想要查詢特定函式的用法時會查詢的文件。

Doxygen 是用於 C、C++、Java 等程式語言的 API 文件生成工具。Doxygen 文件的原始碼是以註解的形式存在於專案原始碼中,所以不需另外準備一份文件檔案。由於 Doxygen 文件的資訊來自註解,不會自動隨著程式碼更新而更新,負責專案的程式人得自己更新 Doxygen 文件的內容。

說明文件則是軟體專案的教程,目的是讓對專案不熟的程式人學習該專案的用法,所以也有推廣專案的用意。由於說明文件可能有多種格式,像是 HTML 文件、PDF 文件、EPUB 電子書等。所以會利用 Sphinx 等說明文件軟體,以單一文件原始碼產生多種格式的說明文件。

版本控制軟體 (Version Control)

版本控制軟體對使用 C 專案來說,不是必備的,但對管理 C 專案卻相當有幫助,尤其是在管理多人協作的專案。版本控制軟體的基本概念是幫軟體專案額外加上狀態,就好像是玩電腦遊戲到某個段落時存取遊戲的狀態般。

當專案具有狀態的概念時,可以在寫錯程式碼的時候將程式碼回溯到先前的版本,也可以在程式碼出現衝突時偵測衝突事件,並提醒程式設計者。除此之外,版本控制還有許多功能,用來處理各式各樣的管理情境。

目前最廣泛使用的版本控制軟體是 Git。像是目前最受歡迎的專案管理網站 GitHub 就是使用 Git 來傳輸專案。另一個知名的專案管理網站 BitBucket 甚至棄用原本有支援的 Mercurial,全面改用 Git 來管理專案。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。