makefile 簡明教程 —— 助你更好理解 C 專案組織

rovast發表於2020-11-14

英文原文地址:www.cs.colby.edu/maxwell/courses/t...

本文原始碼同步在 github.com/rovast/makefile-tutoria...

Makefiles 是組織程式碼編譯的一種方式。通過這篇簡明教程,雖然你不能完整學會 make 指令,但是你可以使用 makefile 來組織小到中型的專案啦。

一個 簡單的例子

我們來從下面的三個檔案開始吧:hellomake.chellofunc.chellomake.h。這是一個經典 C 語言程式,程式碼根據功能組織在不同的檔案中。

hellomake.c

#include <hellomake.h>

int main() {
  // 呼叫另一個檔案裡的函式
  myPrintHelloMake();

  return (0);
}

hellofunc.c

#include <stdio.h>
#include <hellomake.h>

void myPrintHelloMake() {
  printf("Hello makefiles!\n");

  return;
}

hellomake.h

/* example include file */
void myPrintHelloMake(void);

一般情況下,我們通過下面的指令來編譯程式碼:

gcc -o hellomake hellomake.c hellofunc.c -I.

我們來說明下這個指令:

  1. 我們編譯兩個 .c 檔案
  2. 命名了編譯後的可執行檔案為 hellomake
  3. -I. 告訴 gcc 在當前目錄中尋找 hellomake.h

如果沒有使用 makefile,我們在除錯開發的時候,可以在終端上輸入 向上方向鍵 來快速顯示上次的指令(尤其是你有多個 .c 檔案需要編譯的時候)。

然而,通過上面的直接輸入編譯指令的方式存在兩個弊端:

  • 弊端一:不方便呀!當你換了電腦之後,你要重新再輸入上面的指令。
  • 弊端二:編譯效率低下!即使你只是修改了專案中的一個 .c 檔案,每次編譯時,還是需要編譯所有的檔案,這無疑是效率低下,浪費時間。

所以接下來,請出本文的主角 —— makefile。

Makefile1

hellomake: hellomake.c hellofunc.c
    gcc -o hellomake hellomake.c hellofunc.c -I.

把上述的內容,放入到 Makefile 或者 makefile 檔案,然後在命令列輸入 make 命令,就能夠直接執行編譯了。有以下幾點我們需要關注下:

  1. 如果 make 後面沒有跟任何引數,那麼他就會執行 makefile 的第一條規則。
  2. 把命令依賴的檔案放在第一行的 : 後面,這樣 make 就能知道,當依賴檔案變化時, hellomake 規則需要重新執行。
  3. 注意,第二行 gcc 前面,是一個 tab 製表符!不要使用空格!

通過這樣簡單的 Makefile,我們已經解決了弊端一的問題,即:我們不需要每次都輸入編譯指令了。

然而,現在還不夠高效,即使只修改了一個檔案,還是需要全量編譯(即編譯所有的原始檔)。為了使編譯更加高效,讓我們繼續往下看。

Makefile2

CC=gcc
CFLAGS=-I.

hellomake: hellomake.o hellofunc.o
    $(CC) -o hellomake hellomake.o hellofunc.o

我們定義了兩個常量 CCCFLAGS,這兩個常量告訴 make 怎麼去編譯 hellomake.chellofunc.c。其中 CC 告訴 make 使用哪個 C 編譯器,CFLAGS 說明了編譯指令的引數列表。通過把 hellomake.ohellofunc.o 放到依賴列表中, make 指令就知道每次需要分別編譯 .c 檔案,然後再把他們編譯為可行性檔案 hellomake

終端執行效果如下:

➜  makefile-tourial git:(master)make
gcc -I.   -c -o hellomake.o hellomake.c
gcc -I.   -c -o hellofunc.o hellofunc.c
gcc -o hellomake hellomake.o hellofunc.o
➜  makefile-tourial git:(master)

這種形式的 makefile 對小型的專案還是比較方便的。然而,還是有個問題,那就是依賴檔案的更新。設想下,即使你修改了hellomake.h 檔案,make 指令不會重新編譯檔案。

為了解決這個問題,我們需要告訴 make 一件事情:即.c 檔案和 .h 檔案間的依賴關係。好,我們繼續往下看。

Makefile3

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h

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

hellomake: hellomake.o hellofunc.o
    $(CC) -o hellomake hellomake.o hellofunc.o

相較於上個版本,我們先是增加了一個 DEPS:這裡列出了 .c 檔案所依賴的 .h 檔案集合。

接著,我們定義了一個了規則 %.o: %.c $(DEPS):它說明了 .o 檔案是取決於 .c 檔案和 DEPS 裡的 .h 檔案。

接下來我們看下規則 $(CC) -c -o $@ $< $(CFLAGS),意思是說,為了生成這些 .o 檔案,make 指令使用了 CC 定義的編譯器來編譯 .c 檔案:

  • -c 說明了是為了生成目標檔案(object files)
  • $@ 代表 : 左邊的內容,即:%.o
  • $< 是依賴列表裡的第一項,即:%.c
  • CFLAGS 和之前的說明一樣,就是編譯的指令引數了(flag)

執行效果如下:

➜  makefile-tourial git:(master)make
gcc -c -o hellomake.o hellomake.c -I.
gcc -c -o hellofunc.o hellofunc.c -I.
gcc -o hellomake hellomake.o hellofunc.o
➜  makefile-tourial git:(master)

最後,我們再來做下簡化,使編譯更具通用性。我們使用 $@$^ 來分別表示 : 的左側和右側。在下面的例子裡,所有 include 檔案會作為 DEPS 的一部分,所有目標檔案(object files)會作為 OBJ 的一部分。

Makefile4

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o

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

hellomake: $(OBJ)
    $(CC) -o $@ $^ $(CFLAGS)

執行效果如下:

➜  makefile-tourial git:(master)make
gcc -c -o hellomake.o hellomake.c -I.
gcc -c -o hellofunc.o hellofunc.c -I.
gcc -o hellomake hellomake.o hellofunc.o -I.

讓我們來進一步思考下:

  • 我們能不能把 .h 的檔案都放到一個專門的 inlcude 目錄,把 .c 檔案都放到一個專門的 src目錄?
  • 我們能不能把這些煩人的 .o 檔案都隱藏起來?

當然是可以的!我們會在下一個 makefile 中把對應的檔案放到 includelib資料夾中,並且把生成的目標檔案都放到 srcobj 子目錄中。除此之外,我們還可以定義任何我們想包含的庫檔案,比如常用的 math library -lm。這個 makefile 放在 src 目錄裡。

需要注意的是,我們還定義了一個 clean 規則,用來把生成的目標檔案清除(使用 make clean 命令)。.PHONY 防止 make 清除名為 clean 的檔案。

檔案路徑為

➜  src git:(master) ✗ tree          
.
├── hellofunc.c
├── hellomake
├── hellomake.c
├── makefile
└── obj
    ├── hellofunc.o
    └── hellomake.o

1 directory, 6 files

Makefile5

IDIR = ../include
CC=gcc
CFLAGS=-I$(DIR)

ODIR=obj
LDIR=../lib

LIBS=-lm

_DEPS = hellomake.h
DEPS=$(patsubst %,$(IDIR)/%,$(_DEPS))

_OBJ = hellomake.o hellofunc.o
OBJ=$(patsubst %,$(ODIR)/%,$(_OBJ))

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

hellomake: $(OBJ)
    $(CC) -o $@ $^ $(CFLAGS) $(LIBS)

.PHONY: clean

clean:
    rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~

執行結果

➜  src git:(master)make
gcc -c -o obj/hellomake.o hellomake.c -I../include
gcc -c -o obj/hellofunc.o hellofunc.c -I../include
gcc -o hellomake obj/hellomake.o obj/hellofunc.o -I../include

注意要在 src 目錄下執行,並且要把 .h 檔案放到 include 目錄裡

好了,到目前為止,你已經有了一個不錯的 makefile 了,現在你能 hold 住一箇中型的專案了。你也可以增加更多的規則到 makefile 裡,你甚至可以在一個規則中呼叫另一個規則。

想知道更多關於 makefile 和 make 的資訊,就去查閱 GNU Make Manual 吧!

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章