五、.d文件,解決文件間的相互引用
1、自動生成依賴關係
在前文的項目基礎上,考慮一下這種情況:如果我們在w1.h文件裏包含了頭文件w2.h以及w3.h並且用到其中定義的函數。
第一次編譯沒有遇到問題,但是如果後續的開發過程中修改了w2.h或者w3.h文件中的內容,再執行gmake命令的時候,就遇到問題了——w1.cpp文件不會被重新編譯了!
顯然,我們需要將生成目標文件w1.o的規則的依賴項加上w2.h和w3.h。可是如果手動的去檢查每一個文件的引用關係,然後修改Makefile文件,這樣做的效率就太低了。
萬幸的是,編譯器可以幫助我們自動生成依賴關係,只需要在編譯命令中加上“-M”選項,就可以讓編譯器自動尋找源文件中包含的頭文件,並生成一個依賴關係,例如,你可以在shell界面下敲下如下的命令:
g++-MM w1.cpp
可以看到,其輸出爲w1.o:w1.cppw2.h w3.h。這裏需要特別注意的是,我們使用“-MM”而不是“-M”,因爲我們使用的是GUN的C/C++編譯器,使用“-M”參數會將標準庫的頭文件也一併包含進來,但這並不是我們想要的,而使用“-MM”則不會。
現在的問題是,如何利用這個命令去寫好我們的Makefile呢?
GUN組織建議把每一個源文件自動生成的依賴關係放到一個.d文件中,讓每一個.cpp文件都對應一個.d文件,例如之前的w1.cpp,我們可以生成一個w1.d文件,內容爲自動生成的依賴關係 w1.o:w1.cpp w2.h w3.h,然後在Makefile中包含所有的.d文件,我們只需要寫出.cpp文件和.d文件的依賴關係,讓make自動更新或生成.d文件即可。
2、生成.d文件
dep/%.d:%.cpp
@if test ! -d "dep"; then\
mkdir -p dep;\
fi; \
set -e; rm -f $@;
g++ -MM $< > $@.$$$$; \
sed 's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d: /g' < $@.$$$$ > $@; \
rm -f $@.$$$$
在Makefile中加上如上的代碼,就可以生成我們所需要的.d文件了。
又是一堆莫名其妙的符號,我們還是來逐句進行分析。
(1)dep/%.d: %.cpp
使所有的.d文件依賴於對應的.cpp文件,也就是說只要.cpp更新了,我們就重新生成對應的.d文件。這裏和.o文件類似的,我們也創建一個dep目錄用來存放所有的.d文件,既能保持項目文件的整潔和統一,也方便管理。
(2)@if test ! -d "dep"; then\
mkdir -p dep;\
fi; \
檢查當前目錄下是否存在dep目錄,如果不存在,就使用mkdir命令創建dep目錄。
(3)set -e; rm -f $@;
set–e 的作用是如果命令執行出錯就直接退出。$@的含義之前已經說過,這裏rm –f $@的意思就是刪除所有的目標文件。
(4)g++ -MM $< > $@.$$$$; \
$< 的含義是第一個依賴項的名稱,> 是重定向符號,將輸出結果重定向到指定文件中。$@.$$$$ 就是這個文件的文件名,其中“$$$$”表示一個隨機的編號,例如如果有目標文件是w1.d,那麼“$@.$$$$”一個可能的結果就是w1.d.12345。那麼,這句話的含義就是將g++ -MM w1.cpp的輸出結果重定向到w1.d.12345這個文件中。
(5)sed 's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@;\
這裏使用了sed這個工具對文本進行替換處理,單引號中的規則是’s/old/new/g’,s表示替換,末尾的g代表全局的意思,對文本中所有符合要求的字符串進行替換,sed會將符合old模式的字符串替換爲new,具體的使用方法可以查閱一下sed這個工具的幫助文檔。
<$@$$$$,將這個文件的內容作爲sed工具的輸入。
> $@,將sed處理後的內容重定向輸出到這個文件中。
經過這一步的處理後,就把自動生成的依賴關係:
w1.o:w1.cpp w2.hw3.h
轉成:
w1.o w1.d:w1.cppw2.h w3.h
這樣,我們的.d文件也會自動更新啦。
(6)rm -f $@.$$$$
刪除掉這個臨時文件。
3、使用include包含其他文件
在Makefile中我們也可以像在C++文件中那樣包含其他文件。
現在在我們的Makefile中加上這樣一句:
includew1.d
使用這個語句就可以將之前我們生成的.d文件中的內容包含到當前的Makefile中。
當然,也可以用這個命令來包含其他的Makefile文件。具體的用法後面再進行介紹。
我們希望把所有的.d文件都包含在當前的Makefile中。
先定義一個變量,存放所有的.d文件名:
DEPS = $(addsuffix .d,$(addprefix dep/,$(BASE)))
然後使用include$(DEPS) 包含所有的.d文件。
六、-I,引用其他目錄下的.h文件
考慮這種情況:現在有兩個目錄,一個inc目錄用來存放.h文件,一個src目錄,用來存放.cpp文件。
怎麼讓編譯器找到引用的.h文件在哪個目錄下呢?
我們可以使用“-I”選項。 格式爲“-I目錄名”,這樣在編譯的時候,編譯器就會依次到我們指定的目錄中尋找.h文件。
同樣,先定義一個變量,存放所有頭文件的目錄名:
INCLUDEDIR = -I../inc
然後將
g++ -c -o $@$<
這樣的編譯命令中寫成
g++ -c -o $@$(INCLUDEDIR) $<
OK,再來嘗試用gmake命令編譯一下吧,已經可以成功編譯了。
如果需要包含多個目錄下的.h文件,可以重複使用-I選項,中間需要用空格隔開。
七、使用靜態庫
1、修改生成靜態庫的Makefile
有的時候我們不需要生成一個可執行的程序,而是生成一個靜態庫文件,之後在其他的地方引用這個靜態庫文件。
假設我們的項目目錄結構是這樣的,src是項目根目錄,src下面有common和app以及lib兩個目錄,common和app下面都有inc和src兩個目錄。common存放公共庫的源文件,app存放程序源文件,lib存放生成的靜態庫。
修改我們在common目錄下的Makefile文件:
top_srcdir = ../..
#生成靜態庫後所存放的位置
libdir =$(top_srcdir)/lib
#靜態庫文件名
LIBNAME = libfa_common.a
#路徑+靜態庫文件名
TARGET = $(libdir)/$(LIBNAME)
$(TARGET): $(OBJS)
-rm -f $@
ar cr $(TARGET) $(OBJS)
(1) top_srcdir是項目根目錄的路徑,使用相對路徑,方便我們在後面引用其他目錄。
(2) libdir是生成的靜態庫所存放的路徑。
(3) LIBNAME是靜態庫名稱,注意,靜態庫的命名必須以“lib”開頭,以“.a”結尾。
(4) TARGET是目標文件名稱,包含路徑。
(5) 在生成靜態庫文件的規則中,使用ar這個命令。
2、修改引用靜態庫的Makefile
在app/src目錄下的源文件中,編譯的時候需要引用libfa_common.a這個靜態庫,這就需要我們再修改app目錄下的Makefile文件。
這裏使用了兩個新的參數,“-l”和“-L”。
“-l”參數指定要引用的庫的名稱。例如我們要引用libfa_common.a這個靜態庫,那麼需要在編譯命令里加上“-lfa_common”,可以看出,-l後面的庫名稱需要去除前面的“lib”和後面的“.a”。
“-L”參數指定了要引用的庫的目錄,用法和之前的“-I”一樣。這裏需要注意的是,我們需要修改一下VPATH這個變量,指明要引用的靜態庫的目錄。類似這樣:
VPATH:= -L $(top_srcdir)/lib
八、完整的Makefile
其實在每一個目錄下的Makefile中有很多部分是重複的,我們可以考慮將重複的部分提取出來,單獨放在一個公共的Makefile中,然後在其他Makefile中用include包含這個公共的Makefile即可。
我寫了三套Makefile,分別是Makefile(app)、Makefile(lib)、Make.rules。
其中,Make.rules是公共部分,Makefile(app)是用來生成可執行程序的,Makefile(lib)是用來生成靜態庫的,爲了以後遷移方便,考慮到Linux和Unix平臺的差異,以及各個編譯器之間的差異,可以將各種命令也定義成變量,之後使用宏定義進行條件編譯。
貼一下完整的Makefile代碼。
1、Make.rules
#公用Make規則配置
#設置編譯器類型
CXX := g++
CC := gcc
#設置編譯.d文件相關內容
DEPFLAGS := -MM
DEPFILE = $@.$$$$
#設置所有靜態庫文件所在位置,會根據每個Makefile文件的top_srcdir設置相對位置
LIBDIR := $(top_srcdir)/lib
#設置編譯程序時需要在哪些目錄查找靜態庫文件
LDFLAGS := -L.\
-L$(top_srcdir)/lib
#設置VPATH,在檢查依賴關係時,如果查找-lxxxx時,在哪些目錄查找靜態庫文件
VPATH := $(LIBDIR)
#設置編譯程序時查找頭文件的目錄位置
INCLUDEDIR := -I.\
-I../inc\
#聲明要生成的目標文件,具體規則在具體的Makefile中定義
$(TARGET):
#生成.o文件所依賴的.cpp和.c文件
obj/%.o:%.cpp
@if test ! -d "obj"; then\
mkdir-p obj;\
fi;
$(CXX)-c -o $@ $(INCLUDEDIR) $<
obj/%.o:%.c
@iftest ! -d "obj"; then\
mkdir-p obj;\
fi;
$(CC)-c -o $@ $(INCLUDEDIR) $<
#生成.d文件,存放.cpp文件的所有依賴規則
dep/%.d: %.cpp
@iftest ! -d "dep"; then\
mkdir-p dep;\
fi;\
set-e; rm -f $@;
$(CXX)$(DEPFLAGS) $(INCLUDEDIR) $< >$(DEPFILE); \
sed's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@;\
rm-f $@.$$$$
#生成.d文件,存放.c文件的所有依賴規則
dep/%.d: %.c
@iftest ! -d "dep"; then\
mkdir-p dep;\
fi;\
set-e; rm -f $@;
$(CC)$(DEPFLAGS) $(INCLUDEDIR) $< > $(DEPFILE); \
sed's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@; \
rm-f $@.$$$$
include $(DEPS)
#檢測是否有文件被修改,只要有就全部編譯
all: $(SRCS) $(TARGETS)
#清除編譯文件
.PHONY:clean
clean:
-rm-f $(TARGET)
-rm-f obj/*.o
-rm-f dep/*.d
-rm-f core
2、Makefile(lib)
#需要生成靜態庫的Makefile
#程序根目錄
top_srcdir =../../..
#生成靜態庫後所存放的位置
libdir = $(top_srcdir)/lib
#靜態庫文件名
LIBNAME =libfa_common.a
#路徑+靜態庫文件名
TARGET =$(libdir)/$(LIBNAME)
CPP_FILES = $(shell ls *.cpp)
C_FILES = $(-shell ls *.c)
SRCS = $(CPP_FILES) $(C_FILES)
BASE = $(basename $(SRCS))
OBJS = $(addsuffix .o, $(addprefixobj/,$(BASE)))
DEPS = $(addsuffix .d, $(addprefixdep/,$(BASE)))
#包含公共Make規則
include$(top_srcdir)/makeinclude/Make.rules
#設置頭文件及庫文件的位置
INCLUDEDIR := $(INCLUDEDIR)
$(TARGET): $(OBJS)
-rm-f $@
ar cr $(TARGET) $(OBJS)
3、Makefile(app)
#需要生成可執行程序的Makefile
#程序根目錄
top_srcdir =../../..
#目標程序名
TARGET = test
CPP_FILES = $(shell ls *.cpp)
C_FILES = $(-shell ls *.c)
SRCS = $(CPP_FILES) $(C_FILES)
BASE = $(basename $(SRCS))
OBJS = $(addsuffix .o, $(addprefixobj/,$(BASE)))
DEPS = $(addsuffix .d, $(addprefixdep/,$(BASE)))
#包含公共Make規則
include $(top_srcdir)/makeinclude/Make.rules
#額外需要包含的頭文件的目錄位置
INCLUDEDIR := $(INCLUDEDIR)\
-I$(top_srcdir)/src/common/inc\
#所有要包含的靜態庫的名稱
LIBS := -lfa_common
#設置目標程序依賴的.o文件
$(TARGET):$(OBJS) $(LIBS)
-rm-f $@$(CXX)-o $(TARGET) $(INCLUDEDIR) $(LDFLAGS) $(OBJS) $(LIBS)