1. 程式人生 > >(5)Makefile詳解

(5)Makefile詳解

     Makefile是一個自動化的編譯工具,關係到整個工程的編譯規則,極大的提高了軟體開發的效率。

 

    (1)Makefile的編譯規則

//Makefile 也可以寫作 makefile
1)如果這個工程沒有編譯過,那麼我們的所有C檔案都要編譯並被連結。 2)如果這個工程的某幾個C檔案被修改,那麼我們只編譯被修改的C檔案,並連結目標程式。 3)如果這個工程的標頭檔案被改變了,那麼我們需要編譯引用了這個標頭檔案的所有C檔案,並連結目標程式。

 

    (2)Makefile的書寫規則

    規則的三個要素:目標、依賴、命令

 //Makefile格式 
target ... : prerequisites ... command
//解釋
target:目標檔案,可以是object file,也可以是可執行檔案,還可以是一個標籤;
prerequisites:生成target所需要的目標或者檔案;
command:任意shell命令

 

    (3)Makefile的工作原理

//當執行make命令生成目標檔案時
1.make命令使用時會在當前目錄下尋找名為Makefile或者makefile的檔案;
2.如果找到,它將會把檔案中第一個target當作最終的目標檔案;
3.如果target已然存在,並且它所依賴的所有檔案的修改時間都沒更改,則返回;否則執行後面的內容;
4.一層一層往前推,類似一個堆疊,直到到達所依賴的檔案是.h和.c或者.cpp檔案為止,此時便可以鏈式推導生成最終的目標檔案了;

 

    (4)gcc和g++命令概述

    1)gcc和g++簡介

    GCC(GNU Compiler Collection)    GNU編譯器套件

    gcc和g++均是GCC的一部分,gcc是GNU的c編譯器,g++是GNU的c++編譯器;

    gcc將字尾名為.c的檔案當作c程式,將字尾名為.cpp的檔案當作c++程式;

    g++將字尾名為.c和cpp的均當作c++程式;

    2)區別

    g++和gcc均可以編譯連結c或者c++程式,使用方法有稍微的區別,g++編譯連結c程式時可能呼叫gcc;gcc編譯連結c++時,由於c++是c的超集,需要指定使用c++的動態庫libstdc++.so;

    範例如下,是等價的

// 存在一個main.cpp
g++ main.cpp -o test
// 鏈庫的-l引數必須放置在原始碼之後 gcc main.cpp -lstdc++ -o test
gcc main.cpp -o test -lstdc+

    3) GNU的編譯步驟

1.預處理(Preprocessing)
//由前處理器cpp完成,將.cpp原始檔預處理為.i檔案。 g++ -E test.cpp -o test.i 2.編譯(Compilation)。由編譯器cc1plus完成,將.i檔案編譯為.s的彙編檔案。使用-S選項,只進行編譯而不進行彙編,生成彙編程式碼。 g++ -S test.i -o test.s 3.彙編(Assembly)。由彙編器as完成,將.s檔案彙編成.o的二進位制目標檔案。 g++ -c test.s -o test.o 4.連結(Linking)。由連結器ld,將.o檔案連線生成可執行程式。 g++ test.o -o test.out
g++ test.o -o test

 

    (5)Makefile使用簡介

    1)變數的定義和使用

#定義編譯器
cc = gcc 
或者
cc = g++
#定義編譯引數  -w 不顯示任何警告資訊  -W 只顯示錯誤警告資訊  -Wall 顯示所有警告資訊
CFLAG = -g  -Wall -W
#定義待連結的庫 -L後指明待連結的庫所在的路徑 -l引數待連結的庫名
LIBRARY + = -L /usr/lib/ -lstdc++
#新增巨集定義
DEBUG = MYDEF
DEF = -D$(DEBUG) #使用 -o引數建議放在編譯命令最後,否則可能會將原始檔刪除 -g引數必須放置在-o引數之前 $(cc) $(CFLAG) $(DEF) $(原始檔) -o $(目標檔案) $(LIBRARY)

    在變數的使用過程中,分為兩種,遞迴展開式變數和直接展開式變數

//遞迴展開式變數
A = $(B)
B = $(C)
C = ME
則 A = ME,其優點是前面使用的變數可以使用後續定義的變數的值,缺點是有可能陷入無限迴圈
//直接展開式變數
A = aaa
B := $(A)bbb
A = ccc
則 B的值為 aaabbb,後續定義的A的值與其無關
如果寫法為
A = aaa
B = $(A)bbb
A = ccc
則最終 B的值為 cccbbb

    2)$相關變數

$^    所有的依賴目標的集合。以空格分隔。如果在依賴目標中有多個重複的,那個這個變數會去除重複的依賴目標,只保留一份。
$@    表示規則中的目標檔案集。在模式規則中,如果有多個目標,那麼,"$@"就是匹配於目標中模式定義的集合
$?    所有比目標新的依賴目標的集合。以空格分隔。
$<    依賴目標中的第一個目標名字。如果依賴目標是以模式(即"%")定義的,那麼"$<"將是符合模式的一系列的檔案集。注意,其是一個一個取出來的。
$(@D) 表示"$@"的目錄部分(不以斜槓作為結尾),如果"$@"值是"dir/foo.o",那麼"$(@D)"就是"dir",而如果"$@"中沒有包含斜槓的話,其值就是"."(當前目錄) 。 
$(@F) 表示"$@"的檔案部分,如果"$@"值是"dir/foo.o",那麼"$(@F)"就是"foo.o","$(@F)"相當於函式"$(notdir $@)"
// 例1
%.o : %.c
gcc  -c  $<  -o  $@
把所以的c檔案編譯生成對應的o檔案,$<代表每次取的c檔案,$@代表每次c檔案對應的目標檔案
// 例2
main : main.o  test.o  test1.o  test2.o
gcc  -o  $@  $^
把所有的o檔案編譯生成可執行的main檔案,$^代表所以的依賴檔案集合(main.o  test.o  test1.o  test2.o),@代表目標檔案(main)
// 例3
lib : test.o  test1.o  test2.o
ar r lib $?
把有更新的依賴檔案重新打包到庫lib中, 如果只有test1.o更新,則$?代表test1.o, 如果test.o  test1.o都有更新,則$?代表test.o  test1.o的集合。

    3)偽目標 clean build rebuild all

CC := gcc
Target := helloworld.out

$(Target) : func.o main.o
    $(CC) -o $(Target) main.o func.o

main.o : main.c
    $(CC) -c main.c -o main.o
func.o : func.c
    $(CC) -c func.c -o func.o

.PHONY : rebuild clean build

rebuild : clean build

build : $(Target)
#    @echo "build"    
clean :
#    @echo "clean"
    rm *.o $(Target)

    在上述Makefile中,clean偽目標作用是將編譯連結過程中所有生成檔案全部刪除,回到make執行的初始狀態;build的作用是生成target,與make的作用相同;rebuild偽目標依賴於clean 和 build,當用戶輸入make rebuild時,實際上等效於make clean ,make build依此執行,build依賴於target,此時會將target重新生成,整個工程因此重新編譯連結並生成。all偽目標適用於在同一個makefile中生成多個目標庫檔案時使用。

    4)函式

    GNU make提供了很多的函式,可以在Makefile檔案中呼叫這些函式來進行檔名、變數以及命令等的處理。

(1) patsubst  主要對字串進行運算和分析
用法:$(patsubst pattern,replacement,text)
功能:將text文字中出現的所有pattern替換為replacement
例子:$(patsubst %.c,%.o,a.c,b.o,c.c)
輸出:a.o,b.o,c.o
(2) dir 主要用於獲取檔案的路徑
用法:  $(dir text)
功能: 將text中所有檔案的對應目錄輸出
例子: $(dir main.cpp,libstdc++d)
輸出   ./ , /usr/lib/
(3)notdir 抽取除去路徑意外的其它資訊
用法: $(notdir text)
功能: 去除text中所有包含的路徑,只留下檔案資訊
例子: $(notdir /home/perfect/Mywork/C/main.c ./Makefile)
輸出: main.c Makefile
(4) suffix 獲得字尾名
用法:$(suffix text)
功能:將text中所有檔案只留下字尾名
例子:$(suffix a.c,b.c)
輸出 .c,.c
(5) addsuffix 給源目標檔案新增字首
用法:$(addprefix param,text)
功能:將text中每個原始檔新增上合適的字首字尾後輸出
例子:$(addprefix -l,$(LIBS))
輸出:完成的庫名
(6) wildcard 擴充套件萬用字元
用法:$(wildcard PATTERN...)
功能:獲取所有複合PATTERN格式的檔案
例子:$(wildcard *.c)
輸出:當前目錄下所有的.c檔案
複雜例子:$(patsubst %.c,%.o,$(wildcard *.c))
首先獲取當前路徑下所有的.c檔案,然後將.c字尾名更改為.o字尾名並返回;

    5)include關鍵字

    include命令用於將最新的子Makefile包含進當前Makefile檔案,再根據當前Makefile對檔案進行編譯連結;適用於當系統過大時,Makefile複雜時進行拆分。

 

    (6)Makefile簡單範例

//clac_test.h
#ifndef _CALC_TEST_H_ #define _CALC_TEST_H_ namespace test { int add(int a,int b); }
//calc_test.cpp
#include "calc_test.h" namespace test { int add(int a,int b) { return a + b ; } }
//make_test.h
#ifndef _MAKE_TEST_ #define _MAKE_TEST_ #include <iostream> namespace test { class MakeTest { public: void run(); }; } #endif
//make_test.cpp
#include "make_test.h" #include "calc_test.h" namespace test { void MakeTest::run() { int a = 10; int b = 10; std::cout<<test::add(a,b)<<std::endl; } }
//main.cpp
#include "string.h"
#include "make_test.h"

using namespace std;

int main()
{
  test::MakeTest makeTest;// = new MakeTest();
  makeTest.run();
  return 0;
}

    方法1:直接使用g++命令

g++ calc_test.cpp  make_test.cpp main.cpp -o test

    方法2:直接使用gcc命令

gcc calc_test.cpp make_test.cpp main.cpp -lstdc++ -o test

    方法3:全量的Makefile

test : main.o calc_test.o make_test.o
        g++ main.o calc_test.o make_test.o -o test
main.o : main.cpp
        g++ -c main.cpp -o main.o
calc_test.o : calc_test.cpp
        g++ -c calc_test.cpp -o calc_test.o
make_test.o : make_test.cpp
        g++ -c make_test.cpp -o make_test.o
main.cpp : make_test.h
make_test.cpp : make_test.h calc_test.h
calc_test.cpp : calc_test.h
.PHONY : clean
clean :
        rm test *.o

    方法4:Makefile(隱晦推導)

test : main.o calc_test.o make_test.o
        g++ main.o calc_test.o make_test.o -o test
main.o : make_test.h main.cpp
        g++ -c main.cpp make_test.cpp
make_test.o : make_test.h calc_test.h
        g++ -c make_test.cpp calc_test.cpp
calc_test.o : calc_test.h
        g++ -c calc_test.cpp
.PHONY: clean
clean:
        rm test *.o

    方法5:Makefile(變數定義)

objects = calc_test.o make_test.o main.o

test:$(objects)
        gcc -o test $(objects) -lstdc++
$(objects):calc_test.h make_test.h
.PHONY:clean
clean:
        rm test *.o

 

    (7)Makefile處理多目標檔案

    1)單一Makefile利用偽目標 all

     新建檔案test.cpp 編譯為 temp庫

//test.cpp
#include <iostream> int main() { std::cout<<10<<std::endl; return 0; }
//Makefile
all : test temp
temp : test.cpp
        gcc test.cpp -o temp -lstdc++
test : calc_test.cpp make_test.cpp main.cpp
        g++ calc_test.cpp make_test.cpp main.cpp -o test
.PHONY : clean
clean :
        rm test temp

    2)當有多個Makefile時,每個Makefile一個目標檔案

    新建rapidmain.cpp(當前路徑下有rapidxml的所有標頭檔案)

//rapidmain.cpp
#include <iostream> #include "rapidxml_print.hpp" #include "rapidxml_utils.hpp" #include "rapidxml.hpp" using namespace std; int main() { rapidxml::xml_document<> doc; rapidxml::xml_node<> *declaration = doc.allocate_node(rapidxml::node_declaration); declaration->append_attribute(doc.allocate_attribute("version","1.0")); declaration->append_attribute(doc.allocate_attribute("encoding","utf-8")); doc.append_node(declaration); std::cout<<doc<<endl; return 0; }

    新建Makefile.rapidxml檔案

rapid : rapidmain.cpp
        g++ -I ./rapidxml/ rapidmain.cpp -o rapid
.PHONY : clean
        rm rapid

     新建Makefile.maketest檔案

objects = calc_test.o make_test.o main.o

test:$(objects)
        gcc -o test $(objects) -lstdc++
$(objects):calc_test.h make_test.h
.PHONY :clean
clean:
        rm test *.o

    方法1.新建總Makefile(採用偽目標和make命令)

all :
        make -f Makefile.rapidxml
        make -f Makefile.maketest
// make -C [路徑名] 進入指定路徑執行make命令

   方法2.新建總 Makefile(多個目標時,使用include包含子Makefile)

all : test rapid
include Makefile.maketest
include Makefile.rapidxml

&n