makefile快速入門


前言

  在linux上開發c/c++代碼,基本都會使用make和makefile作為編譯工具。我們也可以選擇cmake或qmake來代替,不過它們只負責生成makefile,最終用來進行編譯的依然是makefile。如果你也是c/c++開發人員,無論你使用什么工具,makefile都是必須掌握的。特別是當你打算編寫開源項目的時候,手動編寫一個makefile非常重要。本文的目的就是讓大家快速了解makefile。

了解makefile

   makefile的官方文檔[1] 學習makefile的最佳方式就是直接查閱官方說明

  一般的makefile文件會包含幾個部分:定義變量、目標、依賴、方法段。下面就是一個基礎的makefile大概的樣子:

1 TARGET=test
2 OBJS=main.o foo.o bar.o
3 CC=gcc
4 
5 $(TARGET):$(OBJS)
6     $(CC) $^ -o $@

1-3行定義了變量,第5行冒號前的部分代表目標,表示這部分編譯工作的最終目的。冒號后面的部分是目標的依賴,表示要生成這個目標需要哪些預先准備工作。第6行是方法段,代表具體的方法。第5-6行組成了一個編譯片段。一個makefile可以包含多個編譯片段,方法段也可以有多行。一個編譯片段的依賴可以是其他片段的目標,這樣當執行make的時候,它就會根據依賴關系處理執行次序。一個makefile文件不能出現重名的目標名,且當你執行make的時候,它會默認執行第一條編譯片段,如果第一條編譯片段並沒有其他依賴,make不會繼續向下執行(這一點很重要,后面會有說明)。

  除此以外,makefile還可以通過include的方式包含其它makefile文件,因此我們也可以將公共的部分寫到一起。在makefile里,我們也可以編寫或調用shell腳本。

常見變量和函數介紹

 作為學習前的准備,我們先介紹幾個常見的概念:

1. 關於makefile的命名

你可以使用全小寫或首字母大寫的方式來命名,或者你也可以起任何你喜歡的名字,通過make -f的方式來運行。不過我強烈建議你使用makefile或Makefile,並且在所有的項目中保持統一。

2. 聲明變量和使用變量

makefile中聲明變量的方式是=或:=,使用:=的方式主要是為了處理循環依賴,這個規則可以參考shell腳本。使用變量的方式是$()。除了我們自定義的變量以外,makefile也有預定義的變量。常見的有:

  (1) CC: C編譯器的名稱,默認是cc。通常如果我們是c++程序會改寫它

  (2) CXX: c++編譯器的名稱,默認是g++

  (3) RM: 刪除程序,默認值為rm -f

  (4) CFLAGS: c編譯器的選項,無默認值

  (5) CXXFLAGS: c++編譯器的選項,無默認值

  (6) $*: 不包含擴展名的目標文件名稱

  (7) $+: 所有的依賴文件,以空格分開,並以出現的先后順序,可能包含重復的依賴文件

  (8) $<: 第一個依賴文件的名稱

  (9) $@: 目標文件的完整名稱

  (10) $^: 所有不重復的依賴文件,以空格分開

  (11) MAKE: 就是make命令本身

  (12) CURDIR: makefile的當前路徑

3. 常見函數方法介紹

函數調用是makefile的一大特點,調用的共同方式是將函數名以及入參放在$()中,函數名和參數之間以[空格]分開,參數之間用[逗號]分開。除了makefile預定義的函數以外,我們還可以編寫自己的函數,函數內部使用$(數字)的方式使用參數。

1 define <Funcname>
2     echo $(1) 
3     echo $(2)
4 endef

  (1) call: 自定函數的調用方式,第一個入參是函數名,后面是函數入參

  (2) wildcard: 通配符函數,表示通配某路徑下的所有文件,通常我們是將所有*.cpp或*.h文件選擇出來單獨處理

  (3) patsubst: 替換函數,經常和wildcard聯合使用,例如將*.cpp全部替換成*.o,后文有詳細的使用方法

  (4) foreach: 循環函數,會根據空格將字符串分片處理,我們可以用來處理多個目標的編譯或多個文件路徑的掃描

  (5) notdir: 獲取到路徑的最后一段文件名

  (6) strip: 去掉字符串前后的空格

  (7) shell: 用於在makefile中執行shell腳本

4. 條件分支

  makefile也可以根據條件,選擇不同的處理分支。方式如下:

ifeq ()
else
endif
或者
ifndef
else
endif

條件分支在我的日常開發中不建議使用,因為很容易讓makefile變得晦澀難讀。畢竟是做編譯用的工具,為了方便維護還是不要弄的太復雜。

5. 關於偽目標

A phony target is one that is not really the name of a file; rather it is just a name for a recipe to be executed when you make an explicit request. There are two reasons to use a phony target: to avoid a conflict with a file of the same name, and to improve performance.

對於偽目標官方提供的解釋是這樣的: 偽目標不是一個真實存在的文件名,它只表示了一個編譯的目標。使用偽目標的意義在於:1,避免makefile中的命名重復;2,提高性能。最常用的偽目標就是clean,為了確保我們聲明的目標在makefile路徑下不會重現同名的文件。偽目標的編寫如下:

clean:
    $(RM) $(OBJS) $(TARGET)

.PHONY:clean

多目錄編譯和動態庫

   通常只要我們開發的不是一個demo程序,一個項目都會包含自己的目錄結構,某些項目還包含自己的動態庫需要在編譯時導出。對於多目錄的編譯,網上的方法很多,這里我只介紹一個我個人比較推薦的方式。所有目錄下的源碼都在主makefile中編譯,如果是動態庫目錄則單獨在動態庫所在的目錄下編寫一個makefile,然后讓主目錄中的makefile來調用。和編譯可執行程序不同,編譯動態庫有以下三個注意點:

1. LDLIBS=-shard: 告訴編譯器,需要生成共享庫

2. CXXFLAGS=-fPIC: 這個是C++的編譯選項,在將.cpp生成.o文件的時候,由於通常我們使用自動推導,因此我們需要用這個變量指明編譯要生成與為位置無關的代碼,否則在連接環節會報錯

3. 編譯目標需要以lib開頭.so結尾

一個完整的例子

 下面以一個相對完整的例子作為總結,在這個例子中有對源碼的編譯,也有對動態庫的編譯和導出,還包含了安裝環節。為了方便項目管理,我使用的項目結構如下:

項目
|
-- bin # 可執行程序的所在目錄 | -- include # 內部和外部頭文件的所在目錄。開發初期,這里只會保存外部依賴的頭文件,項目內部的頭文件是在編譯后自動復制進去的,目的是方便在安裝換環節統一處理 | -- lib # 動態庫所在目錄。和include一樣,開發初期只包含依賴的動態庫,項目內部的動態庫是在編譯后復制進去的 | -- src # 源碼目錄

項目源碼如下,你可以直接復制並根據文件頭部注釋中的路徑來生成

./foo/foo.h 和 ./foo/foo.cpp

// ./foo/foo.h
#ifndef FOO_H_
#define FOO_H_

class Foo
{
public:
    explicit Foo();
};

#endif
foo.h
#include "foo.h"
#include <iostream>

using namespace std;

Foo::Foo()
{
    cout << "Create Foo" << endl;
}
foo.cpp

./xthread/xthread.h和./xthread/xthread.cpp

// ./xthread/xthread.h
#ifndef XTHREAD_H
#define XTHREAD_H

#include <thread>
class XThread
{
public:
    virtual void Start();
    virtual void Wait();

private:
    virtual void Main() = 0;
    std::thread th_;
};

#endif
xthread.h
#include "xthread.h"
#include <iostream>

using namespace std;

void XThread::Start()
{
    cout << "Start XThread" << endl;
    th_ = std::thread(&XThread::Main, this);
}

void XThread::Wait()
{
    cout << "Wait XThread Start..." << endl;
    th_.join();
    cout << "Wait XThread End..." << endl;
}
xthread.cpp

./main.cpp

// ./main.cpp
#include <iostream>
#include "foo/foo.h"
#include "xthread.h"

using namespace std;

class XTask : public XThread
{
public:
    void Main() override
    {
        cout << "XTask main start..." << endl;
        this_thread::sleep_for(chrono::seconds(3));
        cout << "XTask main end..." << endl;
    }
};

int main(int argc, char *argv[])
{
    cout << "hello" << endl;
    Foo foo;
    XTask task;
    task.Start();
    task.Wait();
    return 0;
}
main.cpp

main和foo只進行源碼編譯,xthread是動態庫。在編譯順序上,需要先編譯xthread並將頭文件和動態庫文件分別導出到include和lib下,再編譯源碼。最后執行make install,將所有動態庫拷貝至/usr/lib目錄,可執行文件拷貝至/usr/bin目錄。如果你的動態庫還需要給其它項目使用,你還需要將它的頭文件拷貝到/usr/include目錄下。

根據上面介紹的方法,我們首先編寫xthread所在的makefile:

# ./xthread/makefile
TARGET=libxthread.so LDLIBS:=-shared CXXFLAGS:=-std=c++11 -fPIC SRCS:=$(wildcard *.cpp) HEADS:=$(wildcard *.h) OBJS:=$(patsubst %.cpp,%.o,$(SRCS)) $(TARGET):$(OBJS) $(CXX) $(LDFLAGS) $^ -o $@ $(LDLIBS) install:$(TARGET) cp $(TARGET) ../../lib cp $(HEADS) ../../include clean: $(RM) $(OBJS) $(TARGET) .PHONY:clean install

這一步完成以后,makefile可以單獨執行。執行make install會先執行$(TARGET)所在的編譯片段。

編寫主目錄下的makefile,並可以通過主目錄下的makefile控制xthread的編譯執行:

# ./makefile
TARGET=hello
SRC_PATH=$(CURDIR) $(CURDIR)/foo
SRCS=$(foreach dir,$(SRC_PATH),$(wildcard $(dir)/*.cpp))
OBJS=$(patsubst %.cpp,%.o,$(SRCS))
CXXFLAGS=-std=c++11 -I../include 
LDFLAGS=-L../lib
LDLIBS=-lpthread -lxthread
CC=$(CXX)
INSTALL_DIR=/usr

$(TARGET):$(OBJS) depends
    $(CC) $(LDFLAGS) $(OBJS) -o $@ $(LDLIBS)
    @cp $(TARGET) ../bin

depends:
    $(MAKE) install -C $(CURDIR)/xthread -f makefile

install:$(TARGET)
    cp ../bin/$(TARGET) $(INSTALL_DIR)/bin
    cp ../lib/*.so $(INSTALL_DIR)/lib

clean:
    $(RM) $(OBJS) $(TARGET)
    $(MAKE) clean -C $(CURDIR)/xthread

.PHONY: clean install depends

主目錄的$(TARGET)有一個depends,屬於偽目標,會被預先執行。CXXFLAGS表明了編譯需要的外部頭文件的搜索目錄,LDFLAGS表明了外部依賴庫的搜索目錄,LDLIBS說明編譯過程具體需要哪些動態庫。並且會將編譯的可執行文件復制到../bin目錄下。

其它的細節,建議讀者跟着做一遍應該可以掌握。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM