前言
GCC 和 Clang 是兩個在 Unix 或類 Unix 系統上主流的 C 和 C++ 編譯器。在編譯程式時,我們可以使用 GCC 或 Clang 開啟選擇性的警告訊息,藉此了解程式碼潛在的問題。此外,我們可以藉由設置參數,鎖定 C 標準的版本。藉由這些特性,可以改善 C 或 C++ 程式碼的品質。
然而,每次都要手動逐一輸入指令來編譯程式,過於費時費力。即使使用 Make 或其他專案管理軟體,仍然要手動處理設定檔。在本文中,我們使用 shell 命令稿將編譯和執行程式碼的過程自動化,簡化了這項任務。
注意事項
本範例程式會編譯及執行 C 或 C++ 程式碼,故勿將本程式用在有安全疑慮的 (untrusted) 程式碼上。
系統需求
本範例程式的 shell 命令稿以 POSIX shell 寫成。此外,該 shell 命令稿會檢查系統上是否有安裝 GCC 和 Clang。
我們在數種 Unix 或類 Unix 系統上測試此命令稿:
- Ubuntu 18.04 LTS
- CentOS 8
- openSUSE Leap 15.1
- TrueOS (FreeBSD 相容系統)
- Solaris 11
由於本程式使用 POSIX shell,故無法在 Windows 上運行。
使用此命令稿
我們將本文的範例程式包在微型專案 ccwarn 中。ccwarn
是立即可用的程式,而且該程式附有簡單的說明文件。
由於該程式是 shell 命令稿,使用前要先給予該命令稿可執行 (executable) 的權限:
$ chmod +x path/to/ccwarn
在預設情形下,ccwarn
將程式碼視為應用程式,會直接編譯並執行該程式碼:
$ ccwarn path/to/file.c
除了單一程式碼外,ccwarn
也可處理多程式碼:
$ ccwarn path/to/*.c
但這些程式碼要能夠編譯成完整的命令列程式,否則會出錯。
如果想要編譯靜態函式庫,在呼叫 ccwarn
時加上 static
子命令:
$ ccwarn static path/to/*.c
同理,如果想要編譯動態函式庫,在呼叫時 ccwarn
時加上 dynamic
子命令:
$ ccwarn dynamic path/to/*.c
有些程式會混合 C 程式碼和 C++ 程式碼,ccwarn
也可以處理這種型態的程式碼:
$ ccwarn path/to/*.c path/to/*.cpp
如果執行程式時需要參數,可以加上 --
來區隔主程式的參數和目標程式的參數:
$ ccwarn path/to/*.c -- --opt param_a param_b param_c
輸入 help
子命令可取得更多關於 ccwarn
的說明:
$ ccwarn help
在下一節中,我們會講解 ccwarn
的內部實作。如果對實作細節沒興趣,也可以略過講解的部分,把 ccwarn
當成立即可用的程式就好。
此命令稿的內部流程
一開始先檢查程式碼的類型:
if [ "$cmd" = "application" ] \
|| [ "$cmd" = "static" ] \
|| [ "$cmd" = "dynamic" ];
then
shift;
fi
if [ "$cmd" != "application" ] \
&& [ "$cmd" != "static" ] \
&& [ "$cmd" != "dynamic" ];
then
cmd="application";
fi
ccwarn
能處理的類型可能為應用程式 (application)、靜態函式庫 (static library)、動態函式庫 (dynamic library) 三種。預設為應用程式。
接著,設置 C 編譯器指令:
GCC=$GCC;
if [ -z "$GCC" ];
then
GCC="gcc";
fi
在預設情形下,會以 Unix 上常用的慣例為主,例如,GCC 的預設指令即為 gcc
,G++ 的預設指令為 g++
。
ccwarn
會檢查系統上是否有安裝 GCC 和 Clang:
if ! command -v $GCC 2>/dev/null 1>&2;
then
echo "Please install gcc";
exit 1;
fi
當系統上沒有 GCC 和 Clang 時,就無法執行我們設計的任務。這時候會中止程式並吐出相關錯誤訊息。
由於實際執行任務的程式碼很長,我們先將過程寫成虛擬碼:
- iterate over each source file
- check whether file extension is valid
- create object name according to source name
- compile the source into object with a C compiler if it is C
- compile the source into object with a C++ compiler if it is C++
- compile all objects into an executable
- run the executable
我們會把編譯過程分為兩階段,先將原始碼 (source file) 編成目的檔 (object file),再將目的檔編成執行檔 (executable)。由於原始碼可能會混合 C 和 C++,使用這種方式來編譯就可以順利應對。
以下 shell script 從 ccwarn
中節錄出來,並加上行號註解:
if [ "$cmd" = "application" ]; # 1
then # 2
IS_CPP=0; # 3
OBJS=""; # 4
IS_WRONG=0; # 5
for file in $@; # 6
do # 7
if [ "$file" = "--" ]; then # 8
break; # 9
fi # 10
FILE_EXT=$(check_ext "$file"); # 11
if [ "$FILE_EXT" = "$unsupported_file_extension" ]; # 12
then # 13
IS_WRONG=1; # 14
continue; # 15
fi # 16
filename=$(basename "$file"); # 17
objname="${filename%$FILE_EXT}.o"; # 18
OBJS="$objname $OBJS"; # 19
FILE_TYPE=$(check_file_type "$FILE_EXT"); # 20
if [ "$FILE_TYPE" = "c" ]; # 21
then # 22
$GCC -c -o $objname $file \
-Wall -Wextra -std=$CSTD $CFLAGS; # 23
elif [ "$FILE_TYPE" = "c++" ]; # 24
then # 25
IS_CPP=1; # 26
$GXX -c -o $objname $file \
-Wall -Wextra -std=$CXXSTD $CXXFLAGS; # 27
fi # 28
done # 29
if [ $IS_WRONG -ne 0 ]; # 30
then # 31
exit 1; # 32
fi # 33
if [ $IS_CPP -eq 1 ]; # 34
then # 35
LIBS_INTERNAL="-lstdc++"; # 36
fi # 37
$GCC -o $dest $OBJS $LIBS_INTERNAL $LIBS \
-Wall -Wextra -std=$CSTD $CFLAGS $LDFLAGS; # 38
args=""; # 39
is_arg=0; # 40
for arg in $@; do # 41
if [ "$arg" = "--" ]; # 42
then # 43
is_arg=1; # 44
continue; # 45
fi # 46
if [ $is_arg -eq 1 ]; # 47
then # 48
args="$args $arg"; # 49
fi # 50
done # 51
LD_LIBRARY_PATH=$LD_LIBRARY_PATH ./$dest $args; # 52
elif [ "$cmd" = "dynamic" ]; # 53
then # 54
# Compile a dynamic library # 55
elif [ "$cmd" = "static" ]; # 56
then # 57
# Compile a static library # 58
fi # 59
原本的程式碼有三段任務,我們為了簡化程式碼,只保留第一段任務的程式碼。第 53 行後的程式碼已經略去了,只保留 if
敘述的部分。
第 6 行至第 29 行的程式碼在編譯原始碼成目的檔。在編譯目的檔時,會將目的檔移到工作目錄下,所以不需考慮子目錄的議題,但在偶然情形下,會碰到檔名衝突。在這個迴圈中,會根據檔案的副檔名呼叫 C 編譯器或 C++ 編譯器。
第 34 行至第 38 行的程式碼在編譯目的檔成執行檔。我們一律使用 C 編譯器來編譯程式。若碰到 C++ 目的檔,就加上 -lstdc++
。 libstdc++ 是 C++ 標準函式庫,當為我們有編譯到 C++ 目的檔時,就連結此函式庫。
第 39 行至第 51 行的程式碼在解析目標程式的命令列參數。該迴圈會偵測 --
出現的位置,並將之後的命令列參數切割後重接。
第 52 行的程式碼會執行編譯好的程式。
程式結束時,不論目標程式正確與否,皆會清掉編譯輸出。清除輸出的函式如下:
clean ()
{
rm -f $dest "lib${dest}.a" "lib${dest}.so" -- *.o;
}
附註
除了本範例程式外,我們另外做了一個類似的 shell script ccrun。該命令稿可以在不使用自動編譯設定檔的前提下自動編譯及執行 C 或 C++ 程式碼,但不會檢查程式碼。
結語
除了編譯 C 或 C++ 程式碼外,GCC 和 Clang 可以進行靜態程式碼檢查,也可以在編譯時鎖定 C 標準或 C++ 標準的版本。我們應該善用 GCC 和 Clang 的這些特性來檢視 C 或 C++ 程式碼,儘可能減少警告的數量,以改善 C 或 C++ 程式碼的品質。