陷阱:C++模块之间的”直接依赖“和”间接依赖“与Makefile的撰写
參考:http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
參考:http://stackoverflow.com/questions/28011699/makefile-how-to-write-dependency-properly/28013159#28013159
?
--------------------------------------------2015-01-28補(bǔ)充--------------------------------------------
隔了一段時(shí)間之后回過頭來看這里提到的問題,其實(shí)很簡單,我們先來看依賴關(guān)系
首先要明確的是,A.obj一定依賴于A.cpp和A.h,其他依賴就看A.cpp和A.h中的#include ""指令
SportsCar.obj:?SportsCar.cpp?SportsCar.h Car.h // 因?yàn)镾portsCar.h中有#include "Car.h"
Car.obj: Car.cpp Car.h Engine.h // 因?yàn)镃ar.h中有#include "Engine.h"
Engine.obj: Engine.cpp Engine.h Gas.h // 因?yàn)镋ngine.cpp中有#include "Gas.h"
Gas.obj: Gas.cpp Gas.h
我最開始比較疑惑的就是既然SportsCar.h有#include "Car.h",那么不相當(dāng)于SportsCar.h中也有#include "Engine.h"了么,為什么SportsCar.obj不依賴于Engine.h呢?也就是說,如果Engine.h更新了(比如說添加了一些代碼或者刪除了一些代碼),SportsCar.obj會(huì)更新嗎?換句話說,這樣的間接依賴,我們需要寫到Makefile中嗎?
對于問題“SportsCar.obj會(huì)更新嗎”,答案是:不會(huì)(其實(shí)你做個(gè)實(shí)驗(yàn)試試修改一下Engine.h就知道了),仔細(xì)看依賴關(guān)系,如果Engine.h有更新,那么make程序檢測到Engine.h更新之后會(huì)重新生成Engine.obj、Car.obj,但是由于SportsCar.obj并不依賴于Engine.h,所以SportsCar.obj不會(huì)重新生成
對于問題“這樣的間接依賴,我們需要寫到Makefile中嗎”,答案是:如果你確定是“間接依賴”而不是“直接依賴”,那就不用寫到Makefile中,如果是直接依賴,那就必須寫到Makefile中。
好,首先先確定什么是間接依賴和直接依賴:一句話,模塊A的代碼使用了模塊B的API,那么模塊A就直接依賴于模塊B,如果模塊B的代碼使用了模塊C的API,那么模塊A就間接依賴于模塊C(間接依賴是不用寫到Makefile中的)。
這里需要一點(diǎn)定義:什么叫模塊A使用了模塊B的API?我姑且舉幾個(gè)例子:A中使用了B(以及B的組成部分,對于C++中的class而言,B的組成部分包括B的Class name、Member function name以及Member data name以及在B中定義的constant names、Member class names及Member class的組成部分)的名字的地方,都叫A使用了B的API。
比如:Car直接依賴于Engine,因?yàn)镃ar調(diào)用了Engine的API(Car.h在line 9使用了Engine這個(gè)名字,Car.cpp在line 10使用了consumeGas()這個(gè)member function),但是Car是間接依賴于Gas,因?yàn)镃ar.cpp和Car.h中都沒有出現(xiàn)Gas的API,所以在Makefile中,Car.obj只依賴于Engine.h而不依賴于Gas.h
再比如:SportsCar直接依賴于Car,但是SportsCar是間接依賴于Engine,因?yàn)镾portsCar.cpp和SportsCar.h中沒有出現(xiàn)Engine的API,所以在Makefile中SportsCar.obj只依賴于Car.h而不依賴于Engine.h
?
好現(xiàn)在我們對代碼做一些改動(dòng),首先給Engine添加一個(gè)member function(注意同時(shí)修改Engine.h和Engine.cpp):
void Engine::doSomething() {cout << "Engine do something" << endl; }然后在SportsCar.cpp中修改drive(),如下:
void SportsCar::drive() {cout << "SportsCar drive" << endl;Car::drive();engine.doSomething(); }然后重新nmake這個(gè)程序并運(yùn)行,得到如下結(jié)果:
然后刪除Engine的doSomething(),同時(shí)更新Engine.h和Engine.cpp,重新nmake這個(gè)程序,你會(huì)發(fā)現(xiàn)SportsCar.obj沒有被更新(原因很簡單,Makefile中SportsCar.obj不依賴于Engine.h,所以make程序不會(huì)去更新SportsCar.obj),并且在鏈接的時(shí)候報(bào)錯(cuò)undefined reference to Engine::doSomething
?
問題在哪兒?對,因?yàn)樾薷腟portsCar::drive()之后,SportsCar已經(jīng)直接依賴Engine了,原因就是SportsCar::drive()中出現(xiàn)了Engine::doSomething(),根據(jù)我們前面說的,其實(shí)就是SportsCar中使用了Engine的API,從而SportsCar從間接依賴Engine變?yōu)榱酥苯右蕾嘐ngine,所以這時(shí)候就必須在Makefile中令SportsCar.obj依賴于Engine.h,并且(我建議)在SportsCar.cpp中寫上#include "Engine.h"(盡管你不寫也沒關(guān)系,但是我建議還是寫上,為了依賴關(guān)系看起來更明顯)。
?
這個(gè)問題還算好的,好歹linker還給了一個(gè)報(bào)錯(cuò)讓你知道出了問題。像之前提到的那個(gè)問題(“說明”的第5條)本質(zhì)上跟這個(gè)一樣,但是那個(gè)問題,無論linker還是compiler都不會(huì)報(bào)錯(cuò),而你最后就莫名其妙的得到了一個(gè)錯(cuò)誤的程序邏輯和錯(cuò)誤的運(yùn)行結(jié)果。可見在Makefile中正確的寫上依賴關(guān)系的重要性。
?
好,現(xiàn)在可以得出一個(gè)結(jié)論了:
如果模塊A的代碼中出現(xiàn)了模塊B的API,那么我們說模塊A直接依賴于模塊B。
如果A不直接依賴于模塊C,但是模塊B直接依賴于模塊C,那么我們說模塊A間接依賴于模塊C。
如果模塊A直接依賴于模塊B,那么在出現(xiàn)模塊B的API的文件中一定要有#include "B.h"指令,不要利用間接#include的特性。(有一個(gè)例外,那就是你在設(shè)計(jì)一個(gè)C++的class X的時(shí)候,一般是把聲明放在X.h中,把實(shí)現(xiàn)放在X.cpp中,那么這里的建議是:X.cpp只有一條#include指令,也就是#include "X.h",其他所有的#include指令都放到X.h中,這樣可以便于寫Makefile的時(shí)候查看各個(gè)模塊的依賴關(guān)系。這里有個(gè)demo可以參考,比較典型:點(diǎn)此下載demo)
在Makefile中A.obj依賴于B.h當(dāng)且僅當(dāng)模塊A直接依賴于模塊B
?
--------------------------------------------2015-01-28之前--------------------------------------------
總結(jié):
如果A.cpp包含了A.h,E.h,F.h,而A.h又包含了B.h、C.h,那么在Makefile中,A.obj就依賴于A.cpp, A.h, E.h, F.h, B.h, C.h,如果你發(fā)現(xiàn)某個(gè)文件(比如X.h)更新了,但是在rebuild project的時(shí)候與其相關(guān)聯(lián)的文件(比如Y.cpp)沒有被recompile,那么你就要好好檢查是不是Makefile中Y.obj沒有關(guān)聯(lián)X.h,否則就可能導(dǎo)致下面類似的邏輯錯(cuò)誤(而且很難debug)
事實(shí)上,A.obj依賴于XXX.h的充要條件是:A.cpp直接或者間接包含了XXX.h,并且A.cpp中有代碼使用了XXX.h中聲明的東西,并且XXX.h在你開發(fā)的過程中可能被修改(也就是說用#include ""指令包含的文件,因?yàn)橐话?include <>都用于那些庫的頭,而庫的頭在你開發(fā)的過程中一般是不會(huì)被修改的)。但是你在實(shí)際寫Makefile的時(shí)候,你很難無遺漏地判斷A.cpp中是否有代碼使用了XXX.h中聲明的東西,尤其是當(dāng)代碼量很大,工程很復(fù)雜的時(shí)候,這更難辦到。所以最保險(xiǎn)的辦法就是:檢查A.cpp和A.h中的#include ""指令,然后在Makefile中令A(yù).obj依賴于所有這些指令包含的頭文件,并且做到,如果你要使用一個(gè)頭文件中聲明的內(nèi)容,就直接包含這個(gè)頭文件,而不要利用間接包含這個(gè)特性,避免你漏掉某個(gè)依賴關(guān)系,從而給你寫Makefile打下一個(gè)良好的基礎(chǔ)。
------------------------------------------------------
我一直以為,如果一個(gè)A.cpp文件中有多少條 #include "xxx.h"指令,在寫Makefile的時(shí)候A.obj的依賴項(xiàng)除了A.cpp之外,就是A.cpp之內(nèi)所有的 xxx.h
比如,如果A.cpp中有 #include "A.h" #include "B.h" #include "C.h",那么在Makefile中就有:A.obj: A.cpp A.h B.h C.h
但是
下面的例子是說明了,上面的想法是錯(cuò)誤的
先看例子
?
環(huán)境:Windows + Microsoft Visual Studio NMAKE.exe\CL.exe\LINK.exe
?
文件組織(加粗字體的是文件夾):
testproject
src
include
Makefile
src中包含文件:Car.cpp,?SportsCar.cpp, Engine.cpp, Gas.cpp, main.cpp
include中包含文件:Car.h, SportsCar.h, Engine.h, Gas.h
?
概要:Car有一個(gè)Engine,SportsCar是Car的子類,Car.drive()調(diào)用Engine.consumeGas(),Engine.consumeGas()調(diào)用Gas.burn(),SportsCar.drive()重寫了Car.drive()
?
說明:
通過下面的代碼,在Makefile的第29行可以看到,SportsCar.obj只依賴于SportsCar.cpp和SportsCar.h,因?yàn)镾portsCar.cpp只有一條#include "SportsCar.h"的指令
但是,你可以嘗試下面的步驟,就會(huì)發(fā)現(xiàn)問題所在
1、打開VS2013 開發(fā)人員命令提示,切換到,testproject的根目錄,執(zhí)行nmake,生成bin\test.exe
2、輸入bin\test.exe,可以看到輸出結(jié)果如下
3、刪除Car.h的第8、9行,刪除Car.cpp的第10行,保存
4、再次輸入nmake,生成新的bin\test.exe,可以看到,Car.obj和main.obj被重新生成了,因?yàn)檫@2者都依賴于Car.h,其中Car.obj還依賴于Car.cpp,并且鏈接
也沒有問題。輸入bin\test.exe可以看到結(jié)果如下
5、發(fā)現(xiàn)奇怪的地方?jīng)]?沒發(fā)現(xiàn)?好吧。
首先,第3步修改了Car.h之后,其實(shí)很顯然,按C++的語義來講,SportsCar.cpp第10行已經(jīng)是錯(cuò)誤的了,Car都沒有了Engine,作為Car的子類,SportsCar從哪兒來的Engine?(注意SportsCar.h中并沒有定義Engine)。但是由于Makefile中并沒有寫上SportsCar.obj依賴于Car.h的關(guān)系,所以SportsCar.cpp就沒有被重新編譯,SportsCar.obj也沒有被重新生成。這時(shí)候SportsCar.obj已經(jīng)是陳舊的了。
其次,既然SportsCar.obj已經(jīng)是陳舊的了,不符合C++的語義了。為什么在第3步之后,還能鏈接生成bin\test.exe?原因很簡單,鏈接的時(shí)候SportsCar.obj對Engine.obj的鏈接仍然是合法的,因?yàn)镋ngine.consumeGas()仍然存在。另外,SportsCar.obj對Car.obj的鏈接也是合法的,因?yàn)镃ar的constructor和destructor都沒有變,鏈接的時(shí)候,linker主要要檢查的就是SportsCar.obj對Car.obj中方法的調(diào)用,也就是對Car的constructor和destructor的調(diào)用,因?yàn)樵谏珊弯N毀SportsCar對象的時(shí)候會(huì)用到這兩者,顯然,Car的constructor和destructor都存在,所以linker認(rèn)為這是沒有問題的,繼而生成了bin\test.exe。
所以就造成了上面奇怪的運(yùn)行結(jié)果:Car都沒有了Engine,作為Car的子類,SportsCar“平白無故”地有了一個(gè)Engine(注意SportsCar.h中并沒有定義Engine)
6、結(jié)論:如果A.cpp包含了X.h,X.h又包含了Y.h,Y.h又包含了Z.h,那么在寫Makefile的時(shí)候,A.obj依賴的對象不僅有A.cpp, X.h,而且還有Y.h和Z.h(當(dāng)然,對于庫的頭文件就不用寫進(jìn)Makefile了,這里說的頭文件都是你自己在開發(fā)的時(shí)候?qū)懙念^文件,也就是用#include ""指令包含的頭文件。除非你有必要去修改庫的頭文件,才需要把庫的頭文件依賴也放進(jìn)你的Makefile里)
? 實(shí)際上,一般來講,假設(shè)A.cpp包含了B.h,那么很可能A.cpp中會(huì)直接用到B這個(gè)類的某些function,比如說,在A中可能有諸如B.xxx()的調(diào)用,如果B.h包含了比如說X.h,但是A.cpp中沒有代碼直接用到了X,從而X.h的修改并不會(huì)導(dǎo)致A.cpp中的語義出錯(cuò)(因?yàn)锳中沒有代碼直接對X進(jìn)行使用,試想,如果SportsCar.cpp中沒有直接使用Engine.consumeGas(),還會(huì)出現(xiàn)第5步中的情況么?就不會(huì)了!也就是說,如果A.cpp通過B.h間接包含了一個(gè)頭文件X.h,但是在A.cpp中沒有直接使用X.h中的內(nèi)容,那么對X.h的修改就不會(huì)對A.cpp產(chǎn)生影響,A.cpp也不用重新編譯。但是對X.h的修改會(huì)對那些直接使用了X.h中內(nèi)容的文件產(chǎn)生影響(比如說B.cpp如果有代碼直接使用了X.h中的內(nèi)容,那么X.h的修改就會(huì)導(dǎo)致B.cpp的重新編譯,這個(gè)我們顯然是要在Makefile中寫上B.obj依賴于X.h的,所以是沒有問題的)。),那么實(shí)際上Makefile中也不一定非要讓A.obj依賴于X.h。
所以,上面的結(jié)論,準(zhǔn)確來講應(yīng)該是:如果A.cpp包含了X.h,X.h又包含了Y.h,Y.h又包含了Z.h,并且A中的代碼不僅對X進(jìn)行了直接的使用,而且還對Y, Z進(jìn)行了直接的使用,那么在寫Makefile的時(shí)候,A.obj依賴的對象不僅有A.cpp, X.h,而且還有Y.h和Z.h。
當(dāng)然,如果你為了以防萬一也不嫌麻煩的話,還是按照第6步給出的方法寫Makefile吧
我去stackoverflow問了一下,見這個(gè)問題
大概是說msbuild可以解決這個(gè)問題,gcc也有-MM選項(xiàng)可以自動(dòng)生成Makefile中的dependency,但是CL貌似沒有這個(gè)功能
我的想法是,一般來說還是按照,A.cpp以及A.h中有幾個(gè)#include "",A.obj就有幾個(gè)依賴關(guān)系,也就是說,A.cpp有幾個(gè)#include "",在Makefile中A.obj就依賴這幾個(gè)header,如果出現(xiàn)步驟5所述的問題的時(shí)候,才按上面所說的思路去查找Makefile的錯(cuò)誤。
這也是為什么Makefile中第26行,Car.obj依賴于Engine.h的原因
代碼:
Makefile
# compiler CC = cl # linker LINK = link # libraries# headers HEADER_PATH = /I include # options EHSC = /EHsc COMPILATION_ONLY = /c C_OUT = /Fo: L_OUT = /OUT: # compiler & linker debug option, to disable debug, replace '/Zi' & '/DEBUG' with empty strings C_DEBUG = /Zi L_DEBUG = /DEBUG # C_DEBUG = # L_DEBUG = # targets bin\test.exe: bin obj obj\main.obj obj\Car.obj obj\SportsCar.obj obj\Engine.obj obj\Gas.obj$(LINK) $(L_DEBUG) $(L_OUT)bin\test.exe obj\main.obj obj\Car.obj obj\SportsCar.obj obj\Engine.obj obj\Gas.objobj\main.obj: src\main.cpp include\Car.h include\SportsCar.h$(CC) $(C_DEBUG) $(EHSC) $(HEADER_PATH) $(COMPILATION_ONLY) src\main.cpp $(C_OUT)obj\main.objobj\Car.obj: src\Car.cpp include\Car.h include\Engine.h$(CC) $(C_DEBUG) $(EHSC) $(HEADER_PATH) $(COMPILATION_ONLY) src\Car.cpp $(C_OUT)obj\Car.objobj\SportsCar.obj: src\SportsCar.cpp include\SportsCar.h$(CC) $(C_DEBUG) $(EHSC) $(HEADER_PATH) $(COMPILATION_ONLY) src\SportsCar.cpp $(C_OUT)obj\SportsCar.obj obj\Engine.obj: src\Engine.cpp include\Engine.h include\Gas.h$(CC) $(C_DEBUG) $(EHSC) $(HEADER_PATH) $(COMPILATION_ONLY) src\Engine.cpp $(C_OUT)obj\Engine.objobj\Gas.obj: src\Gas.cpp include\Gas.h$(CC) $(C_DEBUG) $(EHSC) $(HEADER_PATH) $(COMPILATION_ONLY) src\Gas.cpp $(C_OUT)obj\Gas.obj# foldersobj:mkdir objbin:mkdir bin# clean.PHONY: clean clean:-rmdir /s /q bin-rmdir /s /q obj-del *.pdb?
main.cpp
1 #include "SportsCar.h" 2 #include "Car.h" 3 #include <iostream> 4 5 using namespace std; 6 7 int main() 8 { 9 Car *car = new Car(); 10 car->drive(); 11 delete car; 12 13 car = new SportsCar(); 14 car->drive(); 15 delete car; 16 17 return 0; 18 }?
Car.h
1 #ifndef CAR_H 2 #define CAR_H 3 4 #include "Engine.h" 5 6 class Car 7 { 8 protected: 9 Engine engine; 10 public: 11 virtual void drive(); 12 virtual ~Car(); 13 }; 14 15 #endif // CAR_H?
Car.cpp
1 #include "Car.h" 2 3 #include <iostream> 4 5 using namespace std; 6 7 void Car::drive() 8 { 9 cout << "Car drive" << endl; 10 engine.consumeGas(); 11 } 12 13 Car::~Car() 14 { 15 // do nothing 16 }?
SportsCar.h
1 #ifndef SPORTSCAR_H 2 #define SPORTSCAR_H 3 4 #include "Car.h" 5 6 class SportsCar : public Car 7 { 8 9 public: 10 void drive(); 11 }; 12 13 #endif // SPORTSCAR_H?
SportsCar.cpp
1 #include "SportsCar.h" 2 3 #include <iostream> 4 5 using namespace std; 6 7 void SportsCar::drive() 8 { 9 cout << "SportsCar drive" << endl; 10 Car::drive(); 11 }?
Engine.h
1 #ifndef ENGINE_H 2 #define ENGINE_H 3 4 class Engine 5 { 6 7 public: 8 void consumeGas(); 9 }; 10 11 #endif // ENGINE_H?
Engine.cpp
1 #include "Engine.h" 2 #include "Gas.h" 3 4 #include <iostream> 5 6 using namespace std; 7 8 void Engine::consumeGas() 9 { 10 cout << "Engine consuming gas" << endl; 11 Gas g; 12 g.burn(); 13 }?
Gas.h
1 #ifndef GAS_H 2 #define GAS_H 3 4 class Gas 5 { 6 7 public: 8 void burn(); 9 }; 10 11 #endif // GAS_H?
Gas.cpp
1 #include "Gas.h" 2 3 #include <iostream> 4 5 using namespace std; 6 7 void Gas::burn() 8 { 9 cout << "Gas burning" << endl; 10 }?
總結(jié)
以上是生活随笔為你收集整理的陷阱:C++模块之间的”直接依赖“和”间接依赖“与Makefile的撰写的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 急用5万怎么借,有以下四种方式
- 下一篇: Oracle 11g RAC featu