描述
在大型项目开发中,往往编译时间非常长,我见过需要编译15分钟的项目,这对于开发人员来说无疑是无奈的等待。如果每次一个小的代码修改,整个项目都要重新编译的话,时间成本是非常高,为了说明这个问题,下面举一个例子:
如下类:
A.hpp
class A { public: void foo(); private: AMember m_member; }
如果该头文件被包含在预编译头文件中,若修改类A,每个.cpp源文件都需要重新编译,因为A.h被预编译头文件包含,所有.cpp可能都需要类A。当我们修改类AMember,所有的文件也需要重新编译。当然上面的结果是我们不想要的,能不能只让包含AMember的源文件重新编译?下面将介绍如何让编译器只编译修改的部分。
类的定义一般包含类的实现细节,如成员变量。上面的例子中包含成员变量AMember,因此A.hpp需要包含头文件AMember.hpp,如#include “AMember.hpp”,这无疑增加了编译的依赖,所以需要移除上面的编译依赖。为了减少这种依赖,下面介绍几种方法。
方法一:利用Handle类
该方法在Scott Meyers写的书籍《EffectiveC++》有描述,一个Hanldle类是包含一个具体实现类的对象,如下:
A.hpp
class AImpl; class A { public: void foo(); private: AImpl * impl;//指向Handle类的指针 };
AImpl.hpp
#include “AMember.hpp” class AImpl { public: void foo(); private: AMember m_member; };
A.cpp
#include "AImpl.hpp" #include “A.hpp” void A::foo() { impl->foo(); }
在头文件A.hpp中采用类前置声明AImpl,没有包含该类的头文件AImpl.hpp,主要是在头文件A.hpp中,只使用指向AImpl的指针,但在A.cpp中需要包含AImpl.hpp。该方法的缺点如下:
1.每个A对象需要一个额外的指针
2.需要在运行是重链成员函数
3.需要动态为AImpl分配内存。
方法二:利用Protocol类
Protocol类是一个抽象类,只代表具体类的一个接口。如下:
B.hpp
class B { public: virtual void foo() = 0; //由于该类为抽象类,不能直接实例化对象,需要提供一个方法实例化 static B * makeB(); };
BImpl.hpp
#include "B.hpp" #include “AMember.hpp” class BImpl : public B { public: void foo(); private: AMember m_member; };
B.cpp
#include "BImpl.hpp" #include "B.hpp" B * B::makeB() { return new BImpl; }
从上面可以看到,源文件B.cpp只需要包含B.hpp和BImpl.hpp。该方法利用一个抽象类,并且提供一个实例化的接口来构造子类BImpl。
该方法的缺点如下:
1.需要一个辅助函数来构造一个对象
2.需要手工释放由辅助函数构造的对象。
3.使用了虚函数,需要提前加入虚表。
方法三:利用模板
D.hpp
template <class T> class TD { public: void foo(); }; // 前置声明一个类 class DImpl; typedef TD<DImpl> D;
DImpl.hpp
#include "D.hpp" class DImpl : public TD<DImpl> { public: void foo(); private AMember m_member; };
D.cpp
#include "DImpl.hpp" void TD<DImpl>::foo() { (static_cast<DImpl *>(this))->foo(); } void DImpl::foo() { m_member.foo(); }
上面的TD的this指针指向DImpl.有了该指针,就可以访问该类的函数。
与方法一对比:
a.不需要额外的变量
c.不需要手工申请和释放内存
与方法二对比:
a.不需要辅助类进行对象实例化,可以通过TD
<DImpl
>.进行实例化。
b.不需要手工释放对象,本方法对象管理和根据自身的初始化方式决定。
注意:基于模板的方法三有一个Bug,当类DImpl包含数据成员时,本方法会失效。AMember的构造函数不会被调用,同时DImpl的构造函数也不会调用,丢失了C++的特性。如果一个类包含数据成员,不能使用方法三。
方法四:利用静态变量
E.hpp
//编译器不需要知道AMember的具体实现 class AMember; class E { public: const AMember& GetMember(); };
E.CPP
#include "E.hpp" #include "AMember.hpp" const AMember& E::GetMember() { static AMember member; return member; } void E::foo() { GetMember().foo }
如果需要使用类AMember,只需要包含E.hpp,然后调用E::GetMember,本方法可以促使自己采用面向对象进行编程,可以减少依赖。由于使用了静态变量是、,类E只有一个实例化对象AMember,有点类似单例模式。
总结上面的方法就是在编译时间和运行时间进行衡量,有的利用运行时间换编译时间,如动态分配内存和释放内存都会损耗运行时间,对于这类运行时间优化可以采用内存池技术。同时虚表也会降低运行速度。本文着重讲解减少编译时源文件之间的依赖。