c++ using 前置声明_C++ 类的前置声明

今天在研究C++”接口与实现分离“的时候遇到了一个问题,看似很小,然后背后的东西确值得让人深思!感觉在学习的过程中有太多的为什么,而每一个为什么背后都隐藏着一些原理和目的,所以得多问自己”为什么“,这样才不仅知其然,更知其所以然,才能举一反三、融会贯通,这也是我以前学习不够重视的一个问题,好在”亡羊补牢,犹未晚也!“。

C++中将”接口与实现分离“的两个重要目的就是”降低文件间的编译依存关系“和”隐藏对象的实现细节“,都是考虑到C++的高效和安全。而实现这一目的的关键技术就是”Pimpl模式(pointer to implementation)”,也即是”把一个类所有的实现细节都“代理”给另一个类来完成,而自己只负责提供接口“,而实现”Pimpl模式”的关键又是“依赖对象的声明(declaration)而非定义(definition)”,这样就引出了今天的话题:为什么通过“依赖对象的声明”可以实现Pimpl模式“,进而实现”接口与实现分离“?我们一步步来抽丝剥茧吧!

什么是“前置声明”?

”前置声明“和”include“就是一对冤家!我们先看一个例子:

[cpp]

// A.h

#include "B.h"

class A

{

public:

A(void);

virtual ~A(void);

private:

B b;

};

// B.h

#include "A.h"

class B

{

private:

A a;

public:

B(void);

~B(void);

};

一编译,就出现了一个互包含的问题了,A中有B类型的成员变量所以需要include,而B中又有A类型的成员变量也需要include,这就导致了循环include,编译是肯定通过不了的!

解决办法就是使用”前置声明“,在A.h中加上class B的声明:

[cpp]

// A.h

#include "B.h"

class B;

class A

{

public:

A(void);

virtual ~A(void);

private:

B b;

};

// B.h

#include "A.h"

class B

{

private:

A a;

public:

B(void);

~B(void);

};

但是,有人知道问题是为什么就被解决的吗,也就是说,加了个前置声明为什么就解决了这样的问题。下面,让我来探讨一下这个前置声明。

前置声明是什么?举个形象点的例子,就是我要盖一个屋子(CHOuse),光有屋子还不行啊,我还得有床(CBed)。但是屋子还没盖好,总不能先买床吧,床的大小我定了,改天买。先得把房子盖好,盖房子的时候我先给床留个位置,等房子盖好了,我再决定买什么样的床。前置声明就是我在声明一个类(CHouse)的时候,用到了另外一个类的定义(CBed),但是CBed还没有定义呢,而且我还先不需要CBed的定义,只要知道CBed是一个类就够了。那好,我就先声明类CBed,告诉编译器CBed是一个类(不用包含CBed的头文件):

class CBed;

然后在CHouse中用到CBed的,都用CBed的指针类型代(因为指针类型固定大小的,但是CBed的大小只用知道了CBed定义才能确定)。等到要实现CHouse定义的时候,就必须要知道CBed的定义了,那是再包好CBed的头文件就行了。

[cpp]

// House.h

class CBed; // 盖房子时:现在先不买,肯定要买床的

class CHouse

{

CBed& bed; // 我先给床留个位置

// CBed bed; // 编译出错

public:

CHouse(void);

CHouse(CBed& bedTmp);

virtual ~CHouse(void);

void GoToBed();

};

// House.cpp

#include "Bed.h"

#include "House.h" // 等房子开始装修了,要买床了

CHouse::CHouse(void)

: bed(*new CBed())

{

CBed* bedTmp = new CBed(); // 把床放进房子

bed = *bedTmp;

}

CHouse::CHouse(CBed& bedTmp)

: bed(bedTmp)

{

}

CHouse::~CHouse(void)

{

delete &bed;

}

void CHouse::GoToBed()

{

bed.Sleep();

}

“前置声明”的作用?

“前置声明”的作用有2:

(1)解决两个class的相互依赖问题,也就是两个文件相互include,减少头文件的包含层次。比如以上的几个例子!

(2)降低文件之间的“编译依存关系”,从第一个例子我们看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。如果使用include的话,一个文件改变,所有include这个文件的文件都得重新编译,这是非常耗时的!

(3)通过“前置声明”可以实现“接口与实现分离“。我们将需要提供给客户的类分割为两个classes:一个只提供接口,另一个负责实现!例如以下实例:

[cpp]

//XYZ.h

class X{

public:

X();

void Print();

};

class Y{

public:

Y();

void Print();

};

class Z{

public:

Z();

void Print();

};

//XYZ.cpp

#include "stdafx.h"

#include "XYZ.h"

#include

X::X(){}

void X::Print(){

std::cout<

}

Y::Y(){}

void Y::Print(){

std::cout<

}

Z::Z(){}

void Z::Print(){

std::cout<

}

// AImpl.h

#include "stdafx.h"

#include "XYZ.h"

class AImpl

{

public:

AImpl();

~AImpl();

X* getX();

Y* getY();

Z* getZ();

void doSth();

private:

X* x;

Y* y;

Z* z;

};

//AImpl.cpp

#include "stdafx.h"

#include "Aimpl.h"

#include

AImpl::AImpl(){

x = new X();

y = new Y();

z = new Z();

}

AImpl::~AImpl(){

if (x)

{

delete x;

}

if (y)

{

delete y;

}

if (z)

{

delete z;

}

}

X* AImpl::getX(){

return x;

}

Y* AImpl::getY(){

return y;

}

Z* AImpl::getZ(){

return z;

}

void AImpl::doSth(){

x->Print();

y->Print();

z->Print();

}

// A.h

#include "stdafx.h"

#include

class AImpl;

class X;

class Y;

class Z;

class A

{

public:

A();

X* getX();

Y* getY();

Z* getZ();

void doSth();

private:

std::shared_ptr pImpl;

};

//A.cpp

#include "stdafx.h"

#include "A.h"

#include "Aimpl.h"

A::A(){

pImpl = std::shared_ptr(new AImpl());

}

X* A::getX(){

return pImpl->getX();

}

Y* A::getY(){

return pImpl->getY();

}

Z* A::getZ(){

return pImpl->getZ();

}

void A::doSth(){

pImpl->doSth();

}

//PreDeclaration.cpp

#include "stdafx.h"

#include "A.h"

int main()

{

A a;

a.doSth();

return 0;

}

在这个例子中,A.h是提供给客户使用的头文件,在该文件中定义了class A,其中只含有一个指针成员(pImpl),指向其实现类(AImpl)。在这样的设计之下,使用class A的客户端就完全与X、Y、Z以及AImpl的实现细节分离了,同时实现了“实现细节”的隐藏,这样,无论这些classes的任何实现修改都不需要A的客户端重新编译。

使用“前置声明”的注意事项

(1)在前置声明时,我们只能使用的就是类的指针和引用,自然也就不能调用对象的方法了!

像我们这样前置声明类A:

class A;

是一种不完整的声明,只要类B中没有执行"需要了解类A的大小或者成员"的操作,则这样的不完整声明允许声明指向A的指针和引用。

而在前一个代码中的语句

A a;

是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含A.h来获得类A的大小,同时也要重新编译类B。

再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是:只要这个声明没有执行“需要了解类A的大小或者成员”的操作就可以了,所以声明成指针或引用是没有“执行需要了解类A的大小或者成员的操作”的。

我们将上面这个例子的A.cpp稍作修改:

[cpp]

//A.cpp

#include "stdafx.h"

#include "A.h"

#include "Aimpl.h"

A::A(){

pImpl = std::shared_ptr(new AImpl());

}

X* A::getX(){

return pImpl->getX();

}

Y* A::getY(){

return pImpl->getY();

}

Z* A::getZ(){

return pImpl->getZ();

}

void A::doSth(){

//pImpl->doSth();

getX()->Print();

getY()->Print();

getZ()->Print();

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值