前言
除了編譯器和編輯器等必要的軟體外,還有許多和撰寫 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++ 使用的靜態程式碼檢查軟體。以下是常見的方案:
- Clang Static Analyzer
- Splint
- cppcheck
- infer (限 GNU/Linux, macOS)
註:目前 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)
如果專案是要對外發佈的,除了寫程式碼外,也要幫專案寫文件。外部專案使用者不會在缺乏足夠的文件、未了解專案如何使用時,自動自發去閱讀專案的程式碼。使用者只會在喜歡或信任該專案,但想進一步了解該專案的實作細節時,才會願意花時間去閱讀專案程式碼。
以下是兩種不同的專案文件撰寫工具:
API 文件如同軟體專案的指引,會詳細地展示每個函式或物件的參數、回傳值等資訊。但不一定會有範例程式。API 文件是對專案已有一定熟悉度,想要查詢特定函式的用法時會查詢的文件。
Doxygen 是用於 C、C++、Java 等程式語言的 API 文件生成工具。Doxygen 文件的原始碼是以註解的形式存在於專案原始碼中,所以不需另外準備一份文件檔案。由於 Doxygen 文件的資訊來自註解,不會自動隨著程式碼更新而更新,負責專案的程式人得自己更新 Doxygen 文件的內容。
說明文件則是軟體專案的教程,目的是讓對專案不熟的程式人學習該專案的用法,所以也有推廣專案的用意。由於說明文件可能有多種格式,像是 HTML 文件、PDF 文件、EPUB 電子書等。所以會利用 Sphinx 等說明文件軟體,以單一文件原始碼產生多種格式的說明文件。
版本控制軟體 (Version Control)
版本控制軟體對使用 C 專案來說,不是必備的,但對管理 C 專案卻相當有幫助,尤其是在管理多人協作的專案。版本控制軟體的基本概念是幫軟體專案額外加上狀態,就好像是玩電腦遊戲到某個段落時存取遊戲的狀態般。
當專案具有狀態的概念時,可以在寫錯程式碼的時候將程式碼回溯到先前的版本,也可以在程式碼出現衝突時偵測衝突事件,並提醒程式設計者。除此之外,版本控制還有許多功能,用來處理各式各樣的管理情境。
目前最廣泛使用的版本控制軟體是 Git。像是目前最受歡迎的專案管理網站 GitHub 就是使用 Git 來傳輸專案。另一個知名的專案管理網站 BitBucket 甚至棄用原本有支援的 Mercurial,全面改用 Git 來管理專案。