pImpl方法是微软的Herb Sutter提出来的,该方法是为了尽量减小接口和实现之间的耦合,以避免接口改动对程序重新编译等带来的影响。简单来说,如果你的大型程序因为复杂的头文件包含关系,使得你对某头文件的某小改动可能引起的巨大编译时间成本望而生畏,那么你需要用pImpl方法来改善这种处境。
下面看两个代码示例。
1、
#include "classA.h"
class B
{
…
classA a;
};
2、
class classA;
class B
{
…
classA* pa;
};
这两个代码都是对classB的声明,可能会在我们工程的classB.h文件中。它们唯一的不同是1直接引用了classA.h头文件,并声明了classA类型的变量a;而2是通过"class classA;"这句进行classA的声明,然后声明了一个classA类型的指针变量pa,很明显,在2的classB.cpp文件中,需要添加#include "classA.h"。
我们来看看1会发生什么事情。如果你的classB.h文件又被其他头文件classC.h引用,而classC.h又被classD.h文件引用,当你的工程足够复杂,你的头文件之间的引用可能会非常杂乱,问题会随之出现,比如重复引用(幸好我们有#ifdef和#pragma once等方法帮我们解决问题),比如交叉引用(a引用b,b引用a,头疼),还有可能引用一些本不需要的头文件。如果你觉得这些问题都可以忽略,那么当你需要改动classA的声明时,不能忽略的问题来了,所有引用该文件的cpp文件都需要重新编译,对于大型工程,这个编译时间成本有时是不能忽略的。你总不能每改动一次classA,就把几乎大部分代码重新编译一次吧,对于开发和调试来说,这几乎不能忍受。
在1中,classA a语句必须要求引用classA的声明,以便给a变量分配空间;而2中的classA* pa语句则不需要知道classA的具体声明,因为任何指针所占用的空间都是一定的。这样,classB与classA之间就没有了耦合,classA的任何改动,对于classB来说都没有影响,除非classB修改了对classA调用的代码。在这里,指针起到了解耦合的作用。
pImpl方法是应该被提倡应用的,如果运用的恰当,对于工程开发、维护和编译都能起到正面作用,对于这种编码习惯,是需要我们在平时就注意培养的。
举例:
class X
{
public :
void Fun();
private :
int i; // add int i; };
// c.h
#include <x.h>
class C
{
public :
void Fun();
private :
X x; // 与X的强耦合
};
PIMPL做法:
// c.h
class X; // 代替#include <x.h>,在c.cpp中 #include <x.h>
class C
{
public :
void Fun();
private :
X *pImpl; // pimpl
};
1)降低模块的耦合。因为隐藏了类的实现,被隐藏的类相当于原类不可见,对隐藏的类进行修改,不需要重新编译原类。
2)降低编译依赖,提高编译速度。指针的大小为(32位)或8(64位),X发生变化,指针大小却不会改变,文件c.h也不需要重编译。
3)接口与实现分离,提高接口的稳定性。
1、通过指针封装,当定义“new C”或"C c1"时 ,编译器生成的代码中不会掺杂X的任何信息。
2、当使用C时,使用的是C的接口(C接口里面操作的类其实是pImpl成员指向的X对象),与X无关,X被通过指针封装彻底的与实现分离。
pImpl( new X()){}
C::~ C()
{
delete pImpl;
pImpl = NULL;
}
void C::Fun()
{
pImpl-> Fun();
}
// main
#include <c.h>
int main()
{
C c1;
c1.Fun();
return 0 ;
}