陷阱: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.cpp和A.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
- #include "SportsCar.h"
- #include "Car.h"
- #include <iostream>
- using namespace std;
- int main()
- {
- Car *car = new Car();
- car->drive();
- delete car;
- car = new SportsCar();
- car->drive();
- delete car;
- return ;
- }
Car.h
- #ifndef CAR_H
- #define CAR_H
- #include "Engine.h"
- class Car
- {
- protected:
- Engine engine;
- public:
- virtual void drive();
- virtual ~Car();
- };
- #endif // CAR_H
Car.cpp
- #include "Car.h"
- #include <iostream>
- using namespace std;
- void Car::drive()
- {
- cout << "Car drive" << endl;
- engine.consumeGas();
- }
- Car::~Car()
- {
- // do nothing
- }
SportsCar.h
- #ifndef SPORTSCAR_H
- #define SPORTSCAR_H
- #include "Car.h"
- class SportsCar : public Car
- {
- public:
- void drive();
- };
- #endif // SPORTSCAR_H
SportsCar.cpp
- #include "SportsCar.h"
- #include <iostream>
- using namespace std;
- void SportsCar::drive()
- {
- cout << "SportsCar drive" << endl;
- Car::drive();
- }
Engine.h
- #ifndef ENGINE_H
- #define ENGINE_H
- class Engine
- {
- public:
- void consumeGas();
- };
- #endif // ENGINE_H
Engine.cpp
- #include "Engine.h"
- #include "Gas.h"
- #include <iostream>
- using namespace std;
- void Engine::consumeGas()
- {
- cout << "Engine consuming gas" << endl;
- Gas g;
- g.burn();
- }
Gas.h
- #ifndef GAS_H
- #define GAS_H
- class Gas
- {
- public:
- void burn();
- };
- #endif // GAS_H
Gas.cpp
- #include "Gas.h"
- #include <iostream>
- using namespace std;
- void Gas::burn()
- {
- cout << "Gas burning" << endl;
- }
陷阱:C++模块之间的”直接依赖“和”间接依赖“与Makefile的撰写的更多相关文章
- Java开发学习(二十九)----Maven依赖传递、可选依赖、排除依赖解析
现在的项目一般是拆分成一个个独立的模块,当在其他项目中想要使用独立出来的这些模块,只需要在其pom.xml使用<dependency>标签来进行jar包的引入即可. <depende ...
- gradle入门(1-8)gradle 的依赖查看、依赖排除和指定版本(需要验证!)
一.依赖查看 gradle dependencies 在gradle dependencies输出会有如下几种标记: 1.版本 : 唯一的依赖. 2.版本():还存在该库其他版本的依赖或者间接依赖,并 ...
- 如何使用 require.js ,实现js文件的异步加载,避免网页失去响应,管理模块之间的依赖性,便于代码的编写和维护。
一.为什么要用require.js? 最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了.后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载.下面的网页代 ...
- 关于gcc、glibc和binutils模块之间的关系,以及在现有系统上如何升级的总结
http://blog.csai.cn/user1/265/archives/2005/2465.html 一.关于gcc.glibc和binutils模块之间的关系 1.gcc(gnu collec ...
- 【转】关于gcc、glibc和binutils模块之间的关系
原文网址:http://www.mike.org.cn/articles/linux-about-gcc-glibc-and-binutils-the-relationship-between-mod ...
- boost::any在降低模块之间耦合性的应用
作者:朱金灿 来源:http://blog.csdn.net/clever101 在开发大型系统中,遵循这样一个原则:模块之间低耦合,模块内高内聚.比如系统中模块有界面模块和算法模块两种,一般是界面模 ...
- ng-repeat循环出来的部分调用同一个函数并且实现每个模块之间不能相互干扰
使用场景:用ng-repeat几个部分,每个部分调用同一个函数,但是每个模块之间的功能不能相互干扰 问题:在用repeat实现.content块repeat的时候打算这样做:新建一个空的数组(nmbe ...
- 多线程(四) 实现线程范围内模块之间共享数据及线程间数据独立(Map集合)
多个线程访问共享对象和数据的方式 1.如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做. 2.如果每个线程执行的代码 ...
- 多线程(三) 实现线程范围内模块之间共享数据及线程间数据独立(ThreadLocal)
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.JDK 1.2的版本中就提供java.lang.ThreadLocal,使用这个工具类可以很简洁地编写出优美的多线程程序,Threa ...
随机推荐
- 如何破解linux用户帐号密码一
ENCRYPT_METHOD SHA512 定义帐号密码的加密方式 1.第一步拿到散列,也就是加密后的密码hash值 2.可以去一些彩虹表(rainbow)网站查询这些hash对应的密码明文,稍微花些 ...
- jdo pom
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...
- utf8汉字编码16进制对照(转载)
utf8汉字编码16进制对照 GB Unicode UTF-8 Chinese CharacterCode code# Code (coded in UTF-8)D2BB 4E00 E4 B8 80 ...
- Zend Guard Run-time support missing 问题的解决
Zend Guard是目前市面上最成熟的PHP源码加密产品了. 刚好需要对自己的产品进行加密,折腾了一晚上,终于搞定,将碰到的问题及解决方法记录下来,方便日后需要,也可以帮助其他人. 我使用的是Wam ...
- ZT:没有谁的成功是横空出世
这世上,没有谁的成功是横空出世. 你看到的胸有成竹,是别人犯过错后的顿悟: 你看到的举重若轻,是别人跌过跤后的自省: 你看到的闪亮光环,是一个人咬牙走了很久的夜路,才为自己点亮的一盏灯. 你以为自己输 ...
- vue - webpack.dev.conf.js
描述:开发时的配置.(配置开发时的一些操作) 例如这里,是否自动打开浏览器(默认true) 'use strict' // build/util.js const utils = require('. ...
- 改动Dialog窗口的类名
VS2013 的MFC project(project名: MobileLink).想要改动窗口的类名时,发现不是像设置窗口名一样调用一个函数能够实现的. 实现的注意问题,请看凝视. (1) 改 ...
- hookup_2.10-0.2.3.jar包下载
hookup_2.10-0.2.3.jar包下载地址,自己也做一个记录.同一时候也给须要的朋友提供一个方便,希望对大家有所帮助.下载地址:http://www.59biye.com/jar/cont/ ...
- Xshell5 破解
Xshell5激活码 Xshell5激活方式Xshell5破解版 Xshell是一个用于MS Windows平台的强大的SSH,TELNET,和RLOGIN终端仿真软件.它使得用户能轻松和安全地从Wi ...
- js 创建数组方法以及区别
示例代码: <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF ...