不懂 Makefile 的程序員還敢寫代碼嗎?

哈嘍大家好,我是小康!

上次我們聊了 GCC、G++ 和 GDB,搞清楚了這些工具在 C/C++ 開發中的用法。有了它們,你已經可以愉快地編譯、調試代碼了。

但我問你,每次編譯項目是不是手動敲gcc 命令? 當項目文件一多,命令就像繞口令一樣——又長又複雜,還特別容易出錯。
別怕,今天我就帶你認識一個 “懶人神器”——Makefile

用 Makefile 的好處很簡單:

只要學會寫 Makefile,編譯這種枯燥的事情,再也不用你操心!讓我們從零開始,一步步帶你搞清楚它是啥、怎麼寫,看完就能用!

1、什麼是 Makefile?

Makefile 就是一個編譯指揮官,你把編譯規則寫在裏面,之後用一條簡單的命令make,它就會按照規則自動完成所有的編譯任務。

打個比方,你是項目經理,Makefile 就是你的筆記本,記錄着項目的 “施工計劃”:

一句話:Makefile 幫你自動化處理那些又多又煩的編譯流程!

2、爲什麼用 Makefile?

假設你有兩個源文件:main.cutils.c,手動編譯步驟大概是這樣:

  1. 先把main.cutils.c 分別編譯成目標文件:
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o
  1. 再把目標文件鏈接成可執行文件:
gcc main.o utils.o -o main

看着簡單,但代碼一多,命令就會變成這樣:

gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc -c file3.c -o file3.o
...
gcc file1.o file2.o file3.o -o my_program

多打一條命令,多一個機會掉坑;一改代碼,又得全編譯一遍,時間都浪費了。
用 Makefile,只需要:

make

一條命令,全搞定!而且它還會只編譯改動的文件,效率直接起飛。

簡單理解:

3、Makefile 的基本結構 (一分鐘搞懂)

Makefile 是由一組規則(rule)組成的,每個規則都包含三部分:

  1. 目標(target):你想要生成的文件,比如main

  2. 依賴(dependencies):目標文件需要哪些源文件或頭文件。

  3. 命令(commands):生成目標需要運行的命令。

舉個例子:

main: main.o utils.o
 gcc main.o utils.o -o main
  
main.o: main.c
    gcc -c main.c

utils.o: utils.c
    gcc -c utils.c

什麼意思呢?

上面總共 3 條規則,來說說第一條規則:

  1. 目標是main,表示我們要生成一個叫main 的可執行文件。

  2. 依賴是main.outils.o,也就是說生成main 需要這兩個依賴文件先生成,而這兩個依賴是利用規則 2 和 規則 3 生成的。

  3. 命令是gcc main.o utils.o -o main,它負責把.o 文件編譯成最終的可執行文件。

後兩條規則類似,告訴make 怎麼生成main.outils.o

簡單嗎?這就相當於告訴 Makefile:“你要先準備好main.outils.o,然後用 gcc 鏈接它們。”

4、Makefile 基礎功能:讓編譯自動化從這裏開始

4.1 自動生成目標文件

如果你每次都向上面一樣手寫main.outils.o 的生成規則,那 Makefile 就會變得非常繁瑣和重複。好消息是,Makefile 支持通配符,可以自動生成規則!

%.o: %.c
 gcc -c $< -o $@

這段代碼怎麼用?假設你有main.cutils.c,Makefile 會自動生成對應的規則:

解釋一下符號:

用這個規則,Makefile 直接幫你生成所有目標文件,舒服吧?

而當項目文件越多,使用 Makefile 的優勢就越大。

4.2 增量編譯:只編譯改動的文件

Makefile 有個超棒的功能:只編譯需要更新的文件
它會檢查每個目標的依賴文件,如果依賴文件沒有變化,就跳過編譯。

比如你改了main.c,Makefile 只會重新生成main.o,而utils.o 完全不動。
這個功能在項目文件很多的時候,能節省一大堆時間。

4.3 清理臨時文件

編譯後,會留下很多.o 文件和中間文件。Makefile 可以加一個clean 規則,幫你一鍵清理:

clean:
 rm -f *.o main

直接運行make clean,乾淨清爽!

5、Makefile 的進階玩法

瞭解了基本用法後,咱們來看一些能提升開發效率的進階功能。

5.1 基礎玩法 - 提高可讀性和可維護性

1. 使用變量:讓 Makefile 更加簡潔

變量怎麼使用?比如CC = gcc。變量可以讓 Makefile 更加靈活和易維護。

變量的基本用法:

# 定義變量
CC = gcc
CFLAGS = -Wall -g
TARGET = main
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o) 

# $(SRCS:.c=.o) 是 Makefile 中的一種變量替換,它的作用是把變量 SRCS 中的每個 .c 文件名換成對應的 .o 文件名。
# 替換之後 OBJS = main.o utils.o

在命令中使用變量時,需要用$() 的形式引用:

$(TARGET): $(OBJS)
 $(CC) $(CFLAGS) $(OBJS) -o $(TARGET)
    
# 使用$()替換變量之後的規則如下:    
main: main.o utils.o
 gcc -Wall -g main.o utils.o -o main

這樣,如果你要修改編譯器或優化選項,只需要改動變量部分,而不需要手動修改每條規則。

2、僞目標:讓 Makefile 更靈活

在 Makefile 中,有些目標(比如clean)不會生成文件,而是用來執行特定的命令,比如清理臨時文件。這種目標我們稱爲 僞目標。

問題來了:如果目錄中剛好有個文件名就叫clean,運行make clean 時,Makefile 會誤以爲這個文件已經存在,導致規則不執行。

怎麼解決?

.PHONY 聲明僞目標,告訴make 這個目標不是文件,應該直接執行命令。

示例:聲明僞目標

.PHONY: clean 
clean:
 rm -f $(OBJS) $(TARGET)

這樣,即使目錄中有個名爲clean 的文件,make clean 仍然會按規則執行,刪除目標文件和中間文件。

記住:凡是不生成文件的目標,都建議用.PHONY 聲明!

5.2  進階玩法 - 構建更強大的 Makefile

1、模式規則:適配更多文件類型

有時候我們的項目裏,不只有.c 文件,還有.cpp 文件。如果要分別寫規則,那就太麻煩了!這時候,模式規則 就能幫上大忙。

什麼是模式規則?

模式規則就是一種通用規則,用來告訴 Makefile:
“遇到這種類型的文件,該怎麼處理。”
比如,告訴 Makefile:

這樣,Makefile 會根據文件後綴自動選擇正確的規則,不用你手動一個一個寫。

怎麼用?支持 C++ 文件

假設項目裏有.cpp 文件,我們可以加一個模式規則:

%.o: %.cpp
 g++ -c $< -o $@

這樣,Makefile 會自動把所有.cpp 文件編譯成.o 文件,完全不用你操心。

如何同時支持 C 和 C++ 文件?

如果項目裏既有.c 文件,也有.cpp 文件,那我們可以寫兩條規則:

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

%.o: %.cpp
 g++ -c $< -o $@

這兩條規則的作用:

  1. 第一條:告訴 Makefile,.c 文件用gcc 編譯。

  2. 第二條:告訴 Makefile,.cpp 文件用g++ 編譯。

這樣,不管你的文件是.c 還是.cpp,Makefile 都會自動搞定。

總結一下:

記住:文件後綴不同?用模式規則全搞定!

2、條件語句:讓 Makefile 更聰明

條件語句可以讓 Makefile 根據實際情況調整規則,比如不同的操作系統、不同的編譯模式,用起來既靈活又省心。

1、適配不同平臺

不同操作系統的命令可能不一樣,比如刪除文件,Linux 用rm,Windows 用del。通過條件語句,Makefile 可以自動選擇正確的命令:

OS = $(shell uname)

ifeq ($(OS), Linux)
 CLEAN_CMD = rm -f
else
 CLEAN_CMD = del
endif

clean:
 $(CLEAN_CMD) *.o $(TARGET)

這樣,無論在哪個平臺都不用手動改命令了,省事!

2、切換編譯模式

開發過程中經常需要在調試模式(debug)和發佈模式(release)之間切換:

用條件語句很容易實現:

ifeq ($(MODE), debug)
 CFLAGS = -g -O0
else
 CFLAGS = -O2
endif

all:
 $(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
make MODE=debug

-g-O0 編譯,生成帶調試信息的程序。

make

或:

make MODE=release

-O2 編譯,生成優化後的高性能程序。

小結一下

3、自動化依賴管理: 讓 Makefile 更聰明!

在寫代碼時,.c 文件往往會用到頭文件.h。比如,你的main.c 裏可能有一句:

#include "utils.h"

如果有一天你修改了utils.h,Makefile 怎麼知道它需要重新編譯main.c 呢?

靠你手動寫依賴規則?別開玩笑了,項目文件一多,光靠手動寫依賴會把人累趴。

這時候,自動化依賴管理就派上用場了。

什麼是自動化依賴管理?

代碼怎麼寫?

看下面這個 Makefile 示例:

# 定義依賴文件列表
DEPS = $(SRCS:.c=.d)

# 生成 .d 文件,寫入依賴規則
%.d: %.c
 $(CC) -M $< > $@

# 包含依賴文件
include $(DEPS)

它到底做了什麼?

  1. 定義依賴文件
DEPS = $(SRCS:.c=.d)

把源文件列表SRCS 中的每個.c 文件,替換成對應的.d 文件,比如:

這些.d 文件就是用來記錄.c.h 之間的關係。

  1. 自動生成依賴規則
%.d: %.c
 $(CC) -M $< > $@

這條規則會用gcc -M 爲每個.c 文件生成一個.d 文件,裏面記錄了它依賴哪些頭文件。

比如,如果你的main.c 包含了utils.h,生成的main.d 文件可能是這樣的:

main.o: main.c utils.h
  1. 包含依賴規則
include $(DEPS)

這句話告訴 Makefile,把所有.d 文件裏的內容加載進來。每次運行 Makefile 時,它都會檢查.d 文件裏的規則,看哪些文件需要重新編譯。

效果如何?

假設你有以下文件:

如果你修改了utils.h,Makefile 會自動發現這個改動,然後只重新編譯main.c,而不會動utils.c

自動化依賴管理的好處:

  1. 再也不用手動寫依賴規則,讓 Makefile 更智能;

  2. 每次頭文件更新時,Makefile 自動判斷哪些文件需要重新編譯;

  3. 即使項目文件多到爆,也能輕鬆應對。

簡單記住:“有.h 文件,就用gcc -M 自動生成依賴!”

4、多目標支持:用 Makefile 管理多個模塊

當你的項目文件越來越多,甚至分成了多個模塊,比如lib 是核心功能模塊,app 是主程序模塊,光靠一個 Makefile 已經很難搞定了。

這時候,聰明的做法是:

分模塊的做法:

1. 給每個模塊單獨寫一個 Makefile

比如,在lib 模塊的目錄下,我們寫一個lib/Makefile

# lib/Makefile
lib.a: lib.o               # 定義目標 lib.a
 ar rcs lib.a lib.o       # 把 lib.o 打包成靜態庫 lib.a

lib.o: lib.c               # 編譯規則:生成 lib.o
 gcc -c lib.c -o lib.o

2. 用主 Makefile 調度所有模塊

主 Makefile 位於項目根目錄,負責把所有模塊串起來。它並不關心每個模塊的具體規則,而是遞歸調用每個模塊自己的 Makefile:

# 主 Makefile
SUBDIRS = lib app          # 定義模塊目錄

all: $(SUBDIRS)            # 主目標:編譯所有模塊

$(SUBDIRS):                # 遞歸調用每個模塊的 Makefile
 $(MAKE) -C $@

clean:                     # 清理所有模塊
 for dir in $(SUBDIRS); do $(MAKE) -C $$dir clean; done

代碼解釋:

  1. SUBDIR 是模塊列表:這裏定義了項目中的模塊,比如libapp。每個模塊目錄都有自己的 Makefile。

  2. $(MAKE) -C $@  是關鍵:這條命令的意思是切換到指定目錄(-C),然後運行這個目錄裏的 Makefile。比如,$(MAKE) -C lib 就是到lib 目錄運行它的 Makefile。

  3. 遞歸清理 : clean 目標會循環進入每個模塊目錄,調用它們的clean 規則。注意$$dir 中的雙$,是爲了讓 Makefile 能正確解析。

整體效果:

用主 Makefile 調度多個模塊的好處:

  1. 結構清晰:每個模塊的規則獨立管理,主 Makefile 只負責調度。

  2. 易於維護:修改或新增模塊時,只需在SUBDIRS 添加對應模塊目錄即可。

  3. 高效遞歸:通過$(MAKE) -C 調用子目錄的 Makefile,模塊間互不干擾。

簡單來說:分模塊管理,用主 Makefile 調度,一切井井有條!

5.3 高階玩法 - 優化效率與靈活性

1. 並行編譯:提高效率

Makefile 的make 命令支持並行執行多個規則,用-j 參數指定並行任務數。

示例:並行編譯

make -j4

這會同時運行最多 4 個任務,充分利用多核 CPU,顯著提升大項目的編譯速度。

2. 自定義函數:複用邏輯

在寫 Makefile 時,如果規則中有重複的編譯邏輯,比如把.c 文件編譯成.o 文件,一直重複寫$(CC) $(CFLAGS) 就很麻煩。這時候,我們可以用自定義函數來統一管理這些重複操作,既方便又省事!

定義函數:

defineendef 定義一個編譯函數:

define compile
 $(CC) $(CFLAGS) -c $< -o $@
endef

使用函數:

調用自定義函數時,用$(call 函數名)

%.o: %.c
 $(call compile)

這條規則會自動把.c 文件編譯成對應的.o 文件。

小結一下:

  1. 自定義函數減少了重複代碼;

  2. 修改邏輯時,只需改函數定義,其他地方不用動;

  3. 讓 Makefile 簡潔易讀,清晰高效。

一句話:把重複的邏輯封裝成函數,Makefile 也能優雅起來!

3. 靜態模式規則:批量生成目標文件

當多個文件需要用相似的規則編譯時,一個個寫太麻煩,用靜態模式規則 就能一次性搞定!

先來看個簡單示例: 假設我們要把多個.c 文件編譯成.o 文件:

OBJS = main.o utils.o io.o

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

這是什麼意思?

優點:

  1. 減少重複:一條規則批量處理,省時省力;

  2. 自動匹配:文件名自動對應,無需手動寫每條規則。

這裏順便提下 通配模式規則,這兩種模式用法很相似。

對於更簡單的項目,你可以用通配模式規則來實現類似效果:

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

解釋一下:

靜態模式規則 vs. 通配模式規則

19etUs

總結:

記住:簡單全局用通配,精準處理選靜態!

4. 跨平臺構建:用 CMake 生成 Makefile

如果項目需要在多個平臺(如 Windows、Linux、macOS)上編譯,直接寫 Makefile 會很麻煩。這時,可以用 CMake 自動生成適配不同平臺的 Makefile。

使用方法:

1. 創建 CMake 配置文件

在項目目錄下新建CMakeLists.txt,內容如下:

# 聲明最低版本要求
cmake_minimum_required(VERSION 3.10)
# 定義項目名稱
project(MyProject)
# 指定可執行文件
add_executable(main main.c utils.c)

2. 生成 Makefile
在終端運行:

cmake .

3. 編譯項目
使用生成的 Makefile:

make

優點:

一句話總結:用 CMake 自動生成 Makefile,跨平臺編譯就是這麼簡單!

五、完整示例

結合前面學到的內容,來看看一個完整的 Makefile:

# 定義變量
CC = gcc                     # 編譯器
CFLAGS = -Wall -g            # 編譯參數:開啓所有警告和調試信息
SRCS =  $(wildcard *.c)      # 獲取當前目錄下所有的 .c 文件,並賦值給 SRCS 變量,例如:SRCS = main.c utils.c 
OBJS = $(SRCS:.c=.o)         # 把 .c 文件替換成 .o 文件,替換之後,OBJS = main.o utils.o 
TARGET = main                # 最終生成的可執行文件

# 編譯規則
$(TARGET): $(OBJS)
 $(CC) $(CFLAGS) $(OBJS) -o $(TARGET)

# 生成 .o 文件規則
%.o: %.c
 $(CC) $(CFLAGS) -c $< -o $@

# 加上 .PHONY 聲明僞目標
.PHONY: clean

clean:
 rm -f $(OBJS) $(TARGET)

使用:

看了這篇文章,相信你看上面的 Makefile 代碼 應該很輕鬆!

六、寫在最後:從 Makefile 開始,走向編譯自動化!

Makefile 就是編譯中的 “懶人神器”,一旦用上,你會發現:

如果你還在手動敲命令,趕緊試試寫個 Makefile,體驗一下自動化的快樂吧~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/u1ftbl3MEhEys0o5dpS1yg