减少编译时源文件之间的依赖
描述在大型项目开发中,往往编译时间非常长,我见过需要编译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.不需要辅助类进行对象实例化,可以通过 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,有点类似单例模式。 总结上面的方法就是在编译时间和运行时间进行衡量,有的利用运行时间换编译时间,如动态分配内存和释放内存都会损耗运行时间,对于这类运行时间优化可以采用内存池技术。同时虚表也会降低运行速度。本文着重讲解减少编译时源文件之间的依赖。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |