本篇介紹如何在 Linux 系統上使用 GCC 編譯器,將寫好的 C 與 C++ 程式碼編譯成執行檔。

Linux 系統上最常見的 C/C++ 編譯器就是 GCC,它是一個開放原始碼的免費編譯器,幾乎任何的 Linux 系統上都有這個編譯器可用,以下介紹 GCC 的基本用法以及範例。

編譯與連結 C 程式

假設我們寫好一個 C 程式,常程式碼儲存在 hello.c 這個檔案中,而其內容如下:

// hello.c
#include <stdio.h>
int main() {
  printf("Hello, world!\n");
  return 0;
}

若要將 C 的原始碼編譯成執行檔,可以執行 gcc 並指定 C 語言的原始碼檔案:

# 編譯 C 程式
gcc hello.c

GCC 預設會執行編譯與連結,直接產生一個可以執行的程式,輸出至 a.out 這個檔案,完成編譯之後,即可執行這個程式:

# 執行編譯好的程式
./a.out
Hello, world!

若要指定輸出的執行檔名稱,可以加上 -o 參數,並指定輸出的檔案名稱:

# 編譯 C 程式,指定輸出檔名
gcc -o hello hello.c

# 執行編譯好的程式
./hello

編譯與連結 C++ 程式

假設我們寫好一個 C++ 的程式儲存在 hello.cpp,內容如下:

// hello.cpp
#include <iostream>
using namespace std;
int main() {
 cout << "Hello, world!" << endl;
 return 0;
}

若要編譯 C++ 的程式,可以使用 g++ 這個編譯程式,並指定 C++ 的原始碼檔案:

# 編譯 C++ 程式
g++ hello.cpp

g++gcc 非常類似,預設的輸出也是 a.out 這個檔案:

# 執行編譯好的程式
./a.out
Hello, world!

若要指定輸出的執行檔名稱,可以加上 -o 參數,並指定輸出的檔案名稱:

# 編譯 C++ 程式,指定輸出檔名
g++ -o hello hello.cpp

# 執行編譯好的程式
./hello

GCC 常用參數

GCC 可用的參數不勝枚舉,這裡我們僅介紹一些初學者常用的參數用法,以及實際的應用範例。

除錯用參數

假設我們寫了一個 C 語言的程式如下:

// bug.c
#include <stdio.h>
int main() {
  int v;  // 未初始化
  printf("v = %d\n", v);
  return 0;
}

其中我們宣告了一個變數 v,但是卻在沒有定義的狀況下直接使用它,向這樣的程式通常都是有問題的,但是按照一般的編譯方式來說,編譯器並不會顯示錯誤,如果在開發過程想要讓編譯器協助偵測這類的問題,可以加上 -Wall 參數,讓編譯器顯示所有的警告訊息。

而通常在開發與除錯的階段,都會使用 gdb 來除錯,所以也會加上 -g 參數(如果沒加的話,就無法用 gdb 除錯)。

所以在開發階段,會很常用到像這樣的指令來編譯程式:

# 顯示警告訊息、加入除錯資訊
g++ -Wall -g -o hello hello.cpp

定義巨集(Macro)

在開發中大型的程式時,開發者通常會使用一些巨集來處理各種的問題,例如除錯時會需要輸出詳細的訊息,但正式版的程式又不需要,這時候就可以這樣寫:

// macro.c
#include <stdio.h>
int main() {
  printf("Hello, world!\n");

#ifdef DEBUG
  printf("DEBUG is defined.\n");
#endif

  return 0;
}

這段程式中,我們用 #ifdef 判斷 DEBUG 這個變數是否有被定義,若有被定義的話,就加入輸出除錯的程式碼,這樣一來我們就可以透過 DEBUG 的定義,來決定是否要產生有除錯訊息的執行檔。

若要定義 DEBUG 這個變數,可將定義直接加在程式碼中:

// 定義 DEBUG
#define DEBUG

也可以直接透過 GCC 的 -D 參數來定義:

# 定義 DEBUG
gcc -DDEBUG macro.c

兩種方式的效果相同,不過使用 GCC 的參數來定義這種時常需要調整的變數,通常會方便很多。

只編譯不連結

GCC 預設會將 C 的原始碼編譯並連結,產生執行檔,若想讓編譯器只進行編譯、不要連結,可加上 -c 參數,這樣就會建立一個 object 檔:

# 僅編譯、不連結,建立 obj 檔案
gcc -c hello.c

執行這行之後,就會產生一個 hello.o 檔案,後續若要進行連結,就可以使用這個 object 檔:

# 連結產生執行檔
gcc -o hello hello.o

通常在大型的專案中,都會將編譯與連結兩個動作拆開,以下是一個典型的例子:

# 編譯個別 C 檔案
gcc -c a.c
gcc -c b.c
gcc -c c.c

# 連結
gcc -o myapp a.o b.o c.o

這樣作除了可讓程式碼方便管理之外,也可以加快編譯的速度,假設我們在開發過程中,更改了 b.c 的內容,在重新編譯時就只要編譯 b.c,然後即可進連結,省去重新編譯 a.cc.c 的時間。

最佳化

GCC 可以根據 CPU 的架構,進行最佳化處理,編譯出效能更好的執行檔,可用的選項有 -O(跟 -O1 相同)、-O2-O3,數字越高代表最佳化的程度越高,許多專案在編譯正式版的時候,都會使用 -O2 進行最佳化。

# 進行最佳化
gcc -O2 -o hello hello.c

標頭檔與函式庫路徑

在編譯程式時,編譯器需要許多標頭檔(*.h 檔案)來編譯原始碼,而連結的時候則會需要一些函式庫(*.a*.so 等)才能進行連結,但是編譯器只會自動引入一些系統預設的檔案,在大型專案中開發者會需要指定許多額外的標頭檔與函式庫位置,這樣才能讓編譯器順利編譯與連結。

編譯器會在 include 路徑中,搜尋 C 程式碼中以 #include 所引入的標頭檔,如果需要指定額外的搜尋路徑,可以使用 -I 參數增加搜尋路徑,假設我們有一些標頭檔放在 /home/gtwang/include 目錄下,而要新增這個路徑就可以這樣寫:

# 新增標頭檔搜尋路徑
gcc -I/home/gtwang/include -o hello hello.c
由於標頭檔的名稱都寫在 C 程式碼中,所以檔名都是已知的,只需要告訴編譯器路徑即可進行編譯。

在連結時期所需要函式庫名稱與路徑,可以透過 GCC 的 -l-L 參數來指定。在 Linux 中假設有一個 libsum.a 函式庫放在 /home/gtwang/lib 目錄下,若要將其納入連結,則可以這樣寫:

# 納入指定的函式庫
gcc -lsum -L/home/gtwang/lib -o hello hello.c

關於 C/C++ 的函數庫製作方式,可以參考建立 C/C++ 靜態、共享與動態載入函式庫教學文章

參考資料:Study-AreaNTU