陷阱: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补充--------------------------------------------

隔了一段时间之后回过头来看这里提到的问题,其实很简单,我们先来看依赖关系

首先要明确的是,A.obj一定依赖于A.cpp和A.h,其他依赖就看A.cppA.h中的#include ""指令

SportsCar.obj: SportsCar.cpp SportsCar.h Car.h // 因为SportsCar.h中有#include "Car.h"

Car.obj: Car.cpp Car.h Engine.h // 因为Car.h中有#include "Engine.h"

Engine.obj: Engine.cpp Engine.h Gas.h // 因为Engine.cpp中有#include "Gas.h"

Gas.obj: Gas.cpp Gas.h

我最开始比较疑惑的就是既然SportsCar.h有#include "Car.h",那么不相当于SportsCar.h中也有#include "Engine.h"了么,为什么SportsCar.obj不依赖于Engine.h呢?也就是说,如果Engine.h更新了(比如说添加了一些代码或者删除了一些代码),SportsCar.obj会更新吗?换句话说,这样的间接依赖,我们需要写到Makefile中吗?

对于问题“SportsCar.obj会更新吗”,答案是:不会(其实你做个实验试试修改一下Engine.h就知道了),仔细看依赖关系,如果Engine.h有更新,那么make程序检测到Engine.h更新之后会重新生成Engine.obj、Car.obj,但是由于SportsCar.obj并不依赖于Engine.h,所以SportsCar.obj不会重新生成

对于问题“这样的间接依赖,我们需要写到Makefile中吗”,答案是:如果你确定是“间接依赖”而不是“直接依赖”,那就不用写到Makefile中,如果是直接依赖,那就必须写到Makefile中。

好,首先先确定什么是间接依赖和直接依赖:一句话,模块A的代码使用了模块B的API,那么模块A就直接依赖于模块B,如果模块B的代码使用了模块C的API,那么模块A就间接依赖于模块C(间接依赖是不用写到Makefile中的)。

这里需要一点定义:什么叫模块A使用了模块B的API?我姑且举几个例子: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,因为Car调用了Engine的API(Car.h在line 9使用了Engine这个名字,Car.cpp在line 10使用了consumeGas()这个member function),但是Car是间接依赖于Gas,因为Car.cpp和Car.h中都没有出现Gas的API,所以在Makefile中,Car.obj只依赖于Engine.h而不依赖于Gas.h

再比如:SportsCar直接依赖于Car,但是SportsCar是间接依赖于Engine,因为SportsCar.cpp和SportsCar.h中没有出现Engine的API,所以在Makefile中SportsCar.obj只依赖于Car.h而不依赖于Engine.h

 

好现在我们对代码做一些改动,首先给Engine添加一个member function(注意同时修改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这个程序并运行,得到如下结果:

然后删除Engine的doSomething(),同时更新Engine.h和Engine.cpp,重新nmake这个程序,你会发现SportsCar.obj没有被更新(原因很简单,Makefile中SportsCar.obj不依赖于Engine.h,所以make程序不会去更新SportsCar.obj),并且在链接的时候报错undefined reference to Engine::doSomething

 

问题在哪儿?对,因为修改SportsCar::drive()之后,SportsCar已经直接依赖Engine了,原因就是SportsCar::drive()中出现了Engine::doSomething(),根据我们前面说的,其实就是SportsCar中使用了Engine的API,从而SportsCar从间接依赖Engine变为了直接依赖Engine,所以这时候就必须在Makefile中令SportsCar.obj依赖于Engine.h,并且(我建议)在SportsCar.cpp中写上#include "Engine.h"(尽管你不写也没关系,但是我建议还是写上,为了依赖关系看起来更明显)。

 

这个问题还算好的,好歹linker还给了一个报错让你知道出了问题。像之前提到的那个问题(“说明”的第5条)本质上跟这个一样,但是那个问题,无论linker还是compiler都不会报错,而你最后就莫名其妙的得到了一个错误的程序逻辑和错误的运行结果。可见在Makefile中正确的写上依赖关系的重要性。

 

好,现在可以得出一个结论了:

如果模块A的代码中出现了模块B的API,那么我们说模块A直接依赖于模块B。

如果A不直接依赖于模块C,但是模块B直接依赖于模块C,那么我们说模块A间接依赖于模块C。

如果模块A直接依赖于模块B,那么在出现模块B的API的文件中一定要有#include "B.h"指令,不要利用间接#include的特性。(有一个例外,那就是你在设计一个C++的class X的时候,一般是把声明放在X.h中,把实现放在X.cpp中,那么这里的建议是:X.cpp只有一条#include指令,也就是#include "X.h",其他所有的#include指令都放到X.h中,这样可以便于写Makefile的时候查看各个模块的依赖关系。这里有个demo可以参考,比较典型:点此下载demo

在Makefile中A.obj依赖于B.h当且仅当模块A直接依赖于模块B

 

--------------------------------------------2015-01-28之前--------------------------------------------

总结:

如果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,如果你发现某个文件(比如X.h)更新了,但是在rebuild project的时候与其相关联的文件(比如Y.cpp)没有被recompile,那么你就要好好检查是不是Makefile中Y.obj没有关联X.h,否则就可能导致下面类似的逻辑错误(而且很难debug)

事实上,A.obj依赖于XXX.h的充要条件是:A.cpp直接或者间接包含了XXX.h,并且A.cpp中有代码使用了XXX.h中声明的东西,并且XXX.h在你开发的过程中可能被修改(也就是说用#include ""指令包含的文件,因为一般#include <>都用于那些库的头,而库的头在你开发的过程中一般是不会被修改的)。但是你在实际写Makefile的时候,你很难无遗漏地判断A.cpp中是否有代码使用了XXX.h中声明的东西,尤其是当代码量很大,工程很复杂的时候,这更难办到。所以最保险的办法就是:检查A.cpp和A.h中的#include ""指令,然后在Makefile中令A.obj依赖于所有这些指令包含的头文件,并且做到,如果你要使用一个头文件中声明的内容,就直接包含这个头文件,而不要利用间接包含这个特性,避免你漏掉某个依赖关系,从而给你写Makefile打下一个良好的基础。

------------------------------------------------------

我一直以为,如果一个A.cpp文件中有多少条 #include "xxx.h"指令,在写Makefile的时候A.obj的依赖项除了A.cpp之外,就是A.cpp之内所有的 xxx.h

比如,如果A.cpp中有 #include "A.h" #include "B.h" #include "C.h",那么在Makefile中就有:A.obj: A.cpp A.h B.h C.h

但是

下面的例子是说明了,上面的想法是错误的

先看例子

 

环境: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有一个Engine,SportsCar是Car的子类,Car.drive()调用Engine.consumeGas(),Engine.consumeGas()调用Gas.burn(),SportsCar.drive()重写了Car.drive()

 

说明:

  通过下面的代码,在Makefile的第29行可以看到,SportsCar.obj只依赖于SportsCar.cpp和SportsCar.h,因为SportsCar.cpp只有一条#include "SportsCar.h"的指令

  但是,你可以尝试下面的步骤,就会发现问题所在

  1、打开VS2013 开发人员命令提示,切换到,testproject的根目录,执行nmake,生成bin\test.exe

  2、输入bin\test.exe,可以看到输出结果如下

  3、删除Car.h的第8、9行,删除Car.cpp的第10行,保存

  4、再次输入nmake,生成新的bin\test.exe,可以看到,Car.obj和main.obj被重新生成了,因为这2者都依赖于Car.h,其中Car.obj还依赖于Car.cpp,并且链接

    也没有问题。输入bin\test.exe可以看到结果如下

  5、发现奇怪的地方没?没发现?好吧。

    首先,第3步修改了Car.h之后,其实很显然,按C++的语义来讲,SportsCar.cpp第10行已经是错误的了,Car都没有了Engine,作为Car的子类,SportsCar从哪儿来的Engine?(注意SportsCar.h中并没有定义Engine)。但是由于Makefile中并没有写上SportsCar.obj依赖于Car.h的关系,所以SportsCar.cpp就没有被重新编译,SportsCar.obj也没有被重新生成。这时候SportsCar.obj已经是陈旧的了。

    其次,既然SportsCar.obj已经是陈旧的了,不符合C++的语义了。为什么在第3步之后,还能链接生成bin\test.exe?原因很简单,链接的时候SportsCar.obj对Engine.obj的链接仍然是合法的,因为Engine.consumeGas()仍然存在。另外,SportsCar.obj对Car.obj的链接也是合法的,因为Car的constructor和destructor都没有变,链接的时候,linker主要要检查的就是SportsCar.obj对Car.obj中方法的调用,也就是对Car的constructor和destructor的调用,因为在生成和销毁SportsCar对象的时候会用到这两者,显然,Car的constructor和destructor都存在,所以linker认为这是没有问题的,继而生成了bin\test.exe。

    所以就造成了上面奇怪的运行结果:Car都没有了Engine,作为Car的子类,SportsCar“平白无故”地有了一个Engine(注意SportsCar.h中并没有定义Engine)

  6、结论:如果A.cpp包含了X.h,X.h又包含了Y.h,Y.h又包含了Z.h,那么在写Makefile的时候,A.obj依赖的对象不仅有A.cpp, X.h,而且还有Y.h和Z.h(当然,对于库的头文件就不用写进Makefile了,这里说的头文件都是你自己在开发的时候写的头文件,也就是用#include ""指令包含的头文件。除非你有必要去修改库的头文件,才需要把库的头文件依赖也放进你的Makefile里)

   实际上,一般来讲,假设A.cpp包含了B.h,那么很可能A.cpp中会直接用到B这个类的某些function,比如说,在A中可能有诸如B.xxx()的调用,如果B.h包含了比如说X.h,但是A.cpp中没有代码直接用到了X,从而X.h的修改并不会导致A.cpp中的语义出错(因为A中没有代码直接对X进行使用,试想,如果SportsCar.cpp中没有直接使用Engine.consumeGas(),还会出现第5步中的情况么?就不会了!也就是说,如果A.cpp通过B.h间接包含了一个头文件X.h,但是在A.cpp中没有直接使用X.h中的内容,那么对X.h的修改就不会对A.cpp产生影响,A.cpp也不用重新编译。但是对X.h的修改会对那些直接使用了X.h中内容的文件产生影响(比如说B.cpp如果有代码直接使用了X.h中的内容,那么X.h的修改就会导致B.cpp的重新编译,这个我们显然是要在Makefile中写上B.obj依赖于X.h的,所以是没有问题的)。),那么实际上Makefile中也不一定非要让A.obj依赖于X.h。

  所以,上面的结论,准确来讲应该是:如果A.cpp包含了X.h,X.h又包含了Y.h,Y.h又包含了Z.h,并且A中的代码不仅对X进行了直接的使用,而且还对Y, Z进行了直接的使用,那么在写Makefile的时候,A.obj依赖的对象不仅有A.cpp, X.h,而且还有Y.h和Z.h。

  当然,如果你为了以防万一也不嫌麻烦的话,还是按照第6步给出的方法写Makefile吧

  我去stackoverflow问了一下,见这个问题

  大概是说msbuild可以解决这个问题,gcc也有-MM选项可以自动生成Makefile中的dependency,但是CL貌似没有这个功能

  我的想法是,一般来说还是按照,A.cpp以及A.h中有几个#include "",A.obj就有几个依赖关系,也就是说,A.cpp有几个#include "",在Makefile中A.obj就依赖这几个header,如果出现步骤5所述的问题的时候,才按上面所说的思路去查找Makefile的错误。

  这也是为什么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.obj

obj\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.obj

obj\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.obj
    
obj\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.obj
    
obj\Gas.obj: src\Gas.cpp include\Gas.h
    $(CC) $(C_DEBUG) $(EHSC) $(HEADER_PATH) $(COMPILATION_ONLY) src\Gas.cpp $(C_OUT)obj\Gas.obj

# folders

obj:
    mkdir obj
    
bin:
    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 }

 

posted @ 2015-01-18 21:04  rldts  阅读(2909)  评论(0编辑  收藏  举报