C++:将文件间的编译依赖性降低的几种方法

C++:将文件间的编译依赖性降低的几种方法

场景

在编写c++代码的时候,我们常常将文件分为头文件(.h) 和 实现文件(.cpp)。 在头文件中的内容我们常常成为接口, 而在实现文件中我们通常写接口的具体实现(当然,这种说法不是特别准确)。当代码稍微复杂一点时, 我们会存在多个这样的文件对, 而且文件间会相互引用。通常我们都是通过#include 操作来在头文件中引入其他头文件。如果在头文件中应用实现文件,那么编译的时候很容易出现重复定义的编译问题。

至此,我们已经将所有源文件大致分为了两大类:.h文件和.cpp文件。那么在编译的时候,通常都是将每个文件对进行编译,然后在将编译的文件进行链接以生成我们需要的输出文件(库或者可执行文件)。那么多的文件对,我们可以自由组合,只要你的组合能够自己“闭合”就能定义成一个输出文件。当然输出文件也能够互相依赖以完成上述的闭合条件。

当我们修改了一个 .h 文件时, 所有利用了这.h 文件的输出文件都会进行重新进行编译生成操作。如果源文件够复杂,存在所有文件都进行编译的可能。而如果修改的是.cpp文件,依赖这个文件的输出文件也会因此重新编译。 但是, .cpp 文件直接被其他文件引用的情况较少,主要是通过包含该文件的输出文件被其他输出文件依赖而导致其他输出文件重新编译。因此头文件中通常写不怎么修改的内容,如函数接口、类等内容,这样就可以在需要改实现文件后尽量减少编译的时间。

因此, 如何尽量减小.h文件的修改造成重新编译的连锁反应是一个需要值得探讨的问题,因为一个较大的源文件系统,编译时间可能是以小时计算的。

考虑下述场景:

// a.h
class A { // something };

//b.h
#include “a.h”
class B
{
	...
public:
	void foo(A a);
private:
	A a;
};

在这种编码情况下, 类A声明的修改必定会引起类B的变化, 从而造成a.h 和b.h 对应的输出文件进行重新编译。如何减小这种连锁反应的发生呢?

方法一

A影响B的关键在于,在编译B时需要知道A的所占用的内存大小,这样才能为B分配合适的空间。如果这个空间是固定的,那么就不管A怎么变化, B均可以不发生变化。因此, 这里可以使用指针来替代B中A的实例。 这种方法需要在构造的时候调用new操作符动态地为该指针分配内存,并需要在析构的时候手动释放内存。

// a.h
class A { // something };

//b.h
class A;
class B
{
	...
public:
	B():a(new A()) {}
	~B() { delete a };
	void foo(A a);
private:
	A* a;
};

方法二

本方法和上述犯法有点类似,知识将一个类分为两个类:接口类和实现类: 接口类保留了实现类的智能指针,间接调用实现类的具体实现。这样我们使用接口类进行include操作,可以减小编译连锁反应的发生。这种设计常被称作pimpl idiom ( pointer to implementation)。这样的class 也被称为 Handle Class。 这种方法相比于上一种方案,使用智能指针,可以自动释放内存。

// a.h
class A { // something };

//b.h
#include <memory>
class A;
class Bimpl;
class B
{
	...
public:
	B():b_impl(new Bimpl()) {}
	void foo(A a) {b_impl->foo(a);}
private:
	std::shared_ptr<Bimpl> b_impl;
};

//b_impl.h
#include "a.h"
class Bimpl
{
	...
	public:
		void foo(A a);
	private:
	A a;
};

上述两种方法的共同点是: 以声明的依存性来代替定义的依存性。尽量以声明式来满足文件的需求,这需要结合引用或者指针来替代对象的实例。如果对象只出现在类的函数接口中,一般使用声明式就行了。

方法三

这中犯法是利用了类的继承关系。将基类设计成抽象基类,即作为接口类, 而其派生类作为具体实现类。因此这样的类也被称作为 interface class。这一类函数通常有一个虚的析构函数和一组作为接口的纯虚函数。

// a.h
class A { // something };

//b.h
#include <memory>
class A;
//  这样的B类对象只能以指针或者应用被使用, 因为许多实现是‘’虚‘’的,不可被实例化。
class B
{
	...
	public:
		virtual ~ B();
		virtual void foo(A a)  =  0;
		static std::shared_ptr<B> create();
};

//工厂函数,用以返回合适的派生类对象指针,当有多个派生类时, 可以通过添加传输参数来区分。
std::shared_ptr<B> B::create() { return std:;shared_ptr<B>( new B_c() ); }

//b_concrete.h
#include "a.h"
#include "b,h"
class B_c: public B
{
	...
public:
	B_c();
	virtual ~B_c();
	void foo(A a);
private:
	A a;
};

方法四

通过多重继承实现interface class。 # To be continue…

Reference

[1] 读书笔记_Effective_C++_条款三十一:将文件间的编译依存关系降至最低(第一部分)
[2] 将文件间的编译依赖降到最低
[3] 如何设计类,可以使得文件间的编译依存关系降至最低

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值