Linux C++ 開發4 - 入門makefile一篇文章就夠了

陌尘(MoChen)發表於2024-08-19
  • 1. make 和 Makefile
    • 1.1. 什麼是make?
    • 1.2. 什麼是Makefile?
    • 1.3. make 與 Makefile的關係
  • 2. Makefile的語法
    • 2.1. 基本語法
    • 2.2. 變數
    • 2.3. 偽目標
    • 2.4. 模式規則
    • 2.5. 自動變數
    • 2.6. 條件判斷
  • 3. 示例演示
    • 3.1. 編譯HelloWorld程式
    • 3.2. 編譯多檔案專案
      • 3.2.1. 專案概述
      • 3.2.2. 需求分析
      • 3.2.3. Makefile V1.0
      • 3.2.4. Makefile V2.0

Linux C++ 開發 系列的前面2篇文章,我們介紹了透過g++來編譯C++程式碼。這對於HelloWorld程式或者簡單的Demo程式來說沒有問題,但對於包含多個.cpp和多個.h檔案的複雜專案來說,直接用g++命令來編譯的話,將會使編譯的指令非常冗長且難於維護。這個時候我們可以考慮用makefile來構建我們的程式。

1. make 和 Makefile

1.1. 什麼是make?

make是一個自動化構建工具,廣泛應用於C/C++專案中,但也可以用於其他程式語言。它的主要功能是根據Makefile中的規則自動執行一系列命令,從而生成目標檔案。make透過比較目標檔案和依賴檔案的時間戳來決定是否需要重新構建某個目標,從而避免了不必要的編譯,提高了構建效率。

我們安裝GCC後,應該預設就已經安裝了make,沒有沒有安裝,Ubuntu下可透過如下命令來安裝:

sudo apt update
sudo apt install make

安裝完成後,你可以透過以下命令來驗證make是否安裝成功:

make --version

1.2. 什麼是Makefile?

Makefile 是一個文字檔案,定義了構建專案的規則和指令。通常定義了多條包含 目標(target)、依賴(dependency)和命令(command) 的規則。

1.3. make 與 Makefile的關係

  • Makefile 你可以理解為是自動構建的指令碼,裡面透過 目標(target)、依賴(dependency)和命令(command) 定義了規則,告訴make工具要如何一步步構建我們的最終目標。
  • make 是一個命令工具,是一個解釋並執行Makefile中指令的命令工具,按照Makefile制定的規則,構建最終的目標產物。

file

2. Makefile的語法

2.1. 基本語法

Makefile的基本語法如下:

目標: 依賴
    命令
  • 目標: 通常是需要生成的檔名,也可以是某個操作(如clean)。
  • 依賴: 生成目標檔案所依賴的其他檔案或其他目標。
  • 命令: 生成目標所需執行的shell命令,必須以Tab鍵開頭。

注意: 命令前面必須是tab鍵,表示命令的開始。不能用4個空格或者兩個空格。

2.2. 變數

在Makefile中,可以使用變數來簡化規則的編寫。變數定義如下:

變數名 = 值

使用變數時,需要在變數名前加上$符號,並用括號括起來:

$(變數名)

2.3. 偽目標

偽目標是一種特殊的目標,它不代表具體的檔案,通常用於執行某些操作。偽目標需要使用.PHONY宣告:

.PHONY: clean
clean:
    rm -f hello hello.o

2.4. 模式規則

模式規則允許定義通用的規則,適用於多個目標。例如:

%.o: %.c
    $(CC) $(CFLAGS) -c $<

這條規則表示所有.o檔案都依賴於對應的.c檔案,並且使用相同的編譯命令。

2.5. 自動變數

make提供了一些自動變數,用於簡化命令的編寫:

  • $@:表示目標檔名。
  • $<:表示第一個依賴檔名。
  • $^:表示所有依賴檔名。

2.6. 條件判斷

Makefile支援條件判斷,可以根據不同的條件執行不同的命令:

ifeq ($(DEBUG),1)
    CFLAGS += -g
else
    CFLAGS += -O2
endif

3. 示例演示

3.1. 編譯HelloWorld程式

我們用Makefile來編譯《Linux C++ 開發2 - 編寫、編譯、執行第一個程式》中的Hello world程式。

Makefile:

# 編譯 demo01.cpp
demo01.out: demo01.cpp
 g++ ./demo01.cpp -o demo01.out

# 申明clean為偽目標
.PHONY: clean
# 定義 clean 命令
clean:
 rm -f demo01.out

編譯和執行:

# 編譯
make
# 執行
./demo01.out
Hello, world!

3.2. 編譯多檔案專案

3.2.1. 專案概述

C++之迭代器》一文中有一個例子是這樣的:

一個公司有多個部門,每個部門有多個人組成,這些人中有開發人員,有測試人員,和與專案相關的其它人員,其結構如下圖片。

file

現在要遍歷這個公司的所有開發人員,遍歷這個公司的所有測試人員。

在這篇文章中,我們用迭代器模式實現了這個需求,類的結構圖是這樣的:

file

詳細程式碼參見: https://gitee.com/spencer_luo/iterator

現在我們就以這個專案為例,看看這個專案的makefile需要怎麼寫?

3.2.2. 需求分析

程式碼結構如下,有三個.cpp,兩個.h,兩個.hpp

依賴關係如下:

Iterator(client) --> Enumerator --> Company --> Department --> Person

./iterator
├── Company.cpp
├── Company.h
├── Department.hpp
├── Enumerator.hpp
├── Iterator.cpp
├── Person.cpp
├── Person.h
└── README.md

3.2.3. Makefile V1.0

# 構建的最終目標 Iterator(可執行檔案)
Iterator:Iterator.o Company.o Person.o
 g++ -o Iterator Iterator.o Company.o Person.o
# 構建目標 Iterator.o
Iterator.o:Iterator.cpp
 g++ -c Iterator.cpp
# 構建目標 Company.o
Company.o:Company.cpp
 g++ -c Company.cpp
# 構建目標 Person.o
Person.o:Person.cpp
 g++ -c Person.cpp

# 申明clean為偽目標
.PHONY: clean
clean:
 rm -f *.o Iterator

執行make進行編譯:

make      
g++    -c -o Iterator.o Iterator.cpp
g++    -c -o Company.o Company.cpp
g++    -c -o Person.o Person.cpp
g++ -o Iterator Iterator.o Company.o Person.o

上面的Makefile大家可能會有一些疑問,這裡對可能存在的疑問做一些解答。

3.2.3.1. 問題一:為什麼沒有標頭檔案的依賴?

問題描述:

如:編譯Person.o時,Person.cpp是包含了Person.h的,為什麼這條規則不寫成:

Person.o:Person.cpp Person.h
 g++ -c Person.cpp

問題解答:

這種寫法也是沒有問題的,對於makefile而言,沒有語法錯誤。但是沒有這個必要,《Linux C++ 開發3 - 你寫的Hello world經過哪些過程才被計算機理解和執行?》一文中我們講了在程式預處理階段,前處理器會將所有透過#include包含的標頭檔案替換成真正的內容,所以我們編譯的時候只需要對.cpp進行編譯即可。

3.2.3.2. 問題二:為什麼沒有對.hpp的規則定義?

問題描述:

為什麼Department.hppEnumerator.hpp不需要編譯。

問題解答:

正常,我們建立C++程式碼檔案的時候,一般會建立兩個檔案:

  • 一個是標頭檔案(如:abc.h),用來進行類、函式、常量等的宣告。
  • 一個是原始檔(如:abc.cpp),用來進行類、函式的定義。

但這樣每次要建立兩個檔案,而且要在兩個檔案上分別進行宣告和定義,挺麻煩的。於是為了偷懶,對於一些簡單的,沒有交叉引用的類,我們通常會把宣告和定義都放在一個檔案中,這個檔案通常以.hpp作為字尾(如:abc.hpp)。

.hpp 本質上還是一個標頭檔案,GCC在編譯的時候,會把它當做標頭檔案來處理。所以我們在Makefile中可以不用寫對.hpp的編譯規則。

3.2.4. Makefile V2.0

GNU的make很強大,它可以自動推導檔案以及檔案依賴關係後面的命令,於是我們就沒必要在每一個.o檔案後都寫上編譯的命令和規則,因為我們的make會自動識別,並自己推導命令。

於是我們的Makefile可以簡化為:

# 構建的最終目標 Iterator(可執行檔案)
Iterator:Iterator.o Company.o Person.o
 g++ -o Iterator Iterator.o Company.o Person.o

# 申明clean為偽目標
.PHONY: clean
clean:
 rm -f *.o Iterator

執行make命令,我們會看到它會自動先去編譯.o, 然後再連結生成最終的二進位制檔案,編譯的過程和V1.0是一樣的。

make      
g++    -c -o Iterator.o Iterator.cpp
g++    -c -o Company.o Company.cpp
g++    -c -o Person.o Person.cpp
g++ -o Iterator Iterator.o Company.o Person.o

大家好,我是陌塵。

IT從業10年+, 北漂過也深漂過,目前暫定居於杭州,未來不知還會飄向何方。

搞了8年C++,也幹過2年前端;用Python寫過書,也玩過一點PHP,未來還會折騰更多東西,不死不休。

感謝大家的關注,期待與你一起成長。



【SunLogging】
Linux C++ 開發4 - 入門makefile一篇文章就夠了
掃碼二維碼,關注微信公眾號,閱讀更多精彩內容

相關文章