描述
在大型项目开发中,往往编译时间非常长,我见过需要编译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.使用了虚函数,需要提前加入虚表。
4.运行时链接虚函数。
方法三:利用模板
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.不需要额外的变量
b.在编译期间链接函数,不需要在运行期间。
c.不需要手工申请和释放内存
与方法二对比:
a.不需要辅助类进行对象实例化,可以通过TD
<DImpl
>.进行实例化。
b.不需要手工释放对象,本方法对象管理和根据自身的初始化方式决定。
c.没有虚表 d.没有虚函数,在编译期间进行链接。
注意:基于模板的方法三有一个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,有点类似单例模式。
总结上面的方法就是在编译时间和运行时间进行衡量,有的利用运行时间换编译时间,如动态分配内存和释放内存都会损耗运行时间,对于这类运行时间优化可以采用内存池技术。同时虚表也会降低运行速度。本文着重讲解减少编译时源文件之间的依赖。