C/C++之类的前置声明

C++中将"接口与实现分离"的两个重要目的就是"降低文件间的编译依存关系"和"隐藏对象的实现细节"。而实现这一目的的关键技术就是Pimpl模式(pointer to implementation),也即是把一个类所有的实现细节都"代理"给另一个类来完成,而自己只负责提供接口。而实现"Pimpl模式"的关键又是"依赖对象的声明(declaration)而非定义(definition)"。那么为什么通过依赖对象的声明可以实现"Pimpl模式",进而实现接口与实现分离?

问题:定义一个类 class A,这个类里面使用了类B的对象b,然后定义了一个类B,里面也包含了一个类A的对象a,就成了这样:

编译一下A.cpp,不通过。再编译B.cpp,还是不通过。编译器都被搞晕了,编译器去编译A.h,发现包含了B.h,就去编译B.h。编译B.h的时候发现包含了A.h,但是A.h已经编译过了(其实没有编译完成,可能编译器做了记录,A.h已经被编译了,这样可以避免陷入死循环。编译出错总比死循环强点),就没有再次编译A.h就继续编译。后面发现用到了A的定义,这下好了,A的定义并没有编译完成,所以找不到A的定义,就编译出错了。

两个文件出现了相互包含的问题,C++是不允许的。解决办法就是使用前置声明而不是直接引用头文件

 

一、前置声明的优点

(1)减小了类的体积

(2)提高了编译速度,因为编译器只需知道该类已经被定义了,而无需了解定义的细节。

使用前置声明可以减少需要包含的头文件。而当一个头文件被包含进来时,相当于引入了新的依赖。只要被依赖的这个头文件有修改,代码就会重新编译。而且这种依赖性是会递归传递的,也就是当头文件A发生了更改时,包含了头文件A的头文件B和所有包含头文件B的头文件都会被重新编译。所以应该适当地减少这种情况的发生。

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

二、何时使用前置声明和#include

首先,我们为什么要包括头文件?答案很简单,通常是我们需要获得某个类型的定义(definition)。那么接下来的问题就是,在什么情况下我们才需要类型的定义,在什么情况下我们只需要声明就足够了?答案是当我们需要知道这个类型的大小或者需要知道它的函数签名的时候,我们就需要获得它的定义。如下,哪些需要C的定义:

  • A继承至C
  • A有一个类型为C的成员变量
  • A有一个类型为C的指针的成员变量
  • A有一个类型为C的引用的成员变量
  • A有一个类型为std::list<C>的成员变量
  • A有一个函数,它的签名中参数和返回值都是类型C
  • A有一个函数,它的签名中参数和返回值都是类型C,它调用了C的某个函数,代码在A的头文件中
  • A有一个函数,它的签名中参数和返回值都是类型C(包括类型C本身,C的引用类型和C的指针类型),并且它会调用另外一个使用C的函数,代码直接写在A的头文件中
  • C和A在同一个名字空间里面
  • C和A在不同的名字空间里面

情况一:必须要知道C的定义,因为A作为子类,必须要知道C的内容,才能继承

情况二:必须要知道C的定义,因为需要根据C来确定A的大小,一般用Pimpl模式 改善。

情况三和情况四:不需要知道C的定义,只需要前置声明就可以了。引用在物理上也是一个指针,效果和指针一样。即便没有C的定义,A也不会有任何问题。

情况五:不需要知道C的定义,有可能老式的编译器需要。标准库里面的容器(如:list、vector、map),在包括一个list<C>,vector<C>,map<C, C>类型的成员变量的时候,都不需要C的定义。因为它们内部其实也是使用C的指针作为成员变量,它们的大小一开始就是固定的了,不会根据模版参数的不同而改变。

情况六:不需要知道C的定义

情况七:必须要知道C的定义,需要知道调用函数的签名。

情况八:对于引用和指针情况一样

例如:

类C中有:C& CdoSomething(C&);

类A中有:C& AdoSomething (C& c) { return CdoSomething (c);};

以上情况,不需要知道C的定义,但是对上面的函数任意一个C&换成C,比如像下面的几种示例:

类C中有:C& CdoSomething (C&);

类A中有:C& AdoSomething (C c) {return CdoSomething (c);};

类C中有:C& CdoSomething (C);

类A中有:C& AdoSomething (C& c) {return CdoSomething (c);};

类C中有:C CdoSomething (C&);

类A中有:C& AdoSomething (C& c) {return CdoSomething (c);};

类C中有:C& CdoSomething (C&);

类A中有:C AdoSomething (C& c) {return CdoSomething (c);};

那么就必须要C的定义。无论哪一种,其实都隐式包含了一个拷贝构造函数的调用,比如1中参数c由拷贝构造函数生成,3中CdoSomething的返回值是一个由拷贝构造函数生成的匿名对象。因为我们调用了C的拷贝构造函数,所以以上无论那种情形都需要知道C的定义。

情况九和情况十:不需要知道C的定义。

二、结论

(1)前置声明只能作为指针引用,不能定义类的对象,自然也就不能调用对象中的方法了。

(2)而且需要注意,如果将类A的成员变量B* b;改写成B& b;的话,必须要将b在A类的构造函数中,采用初始化列表的方式初始化,否则也会出错。

(3)尽量不要在头文件中包含另外的头文件。尽量在头文件中使用类前置声明程序下文中要用到的类,实在需要包含其它的头文件时,可以把它放在我们的类实现文件(cpp)中。

正常结构的C++如下:

// House.h

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

classCHouse

{

private:

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

// CBed bed; // 编译出

public:

CHouse(void);

CHouse(CBed &bedTmp);

virtual~CHouse(void);

voidGoToBed();

};

 

// House.cpp

#include"Bed.h"

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

 

CHouse::CHouse(void)

: bed(*newCBed()) // 这里对引用的赋

{

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

bed = *bedTmp;

}

CHouse::CHouse(CBed &bedTmp)

: bed(bedTmp)

{

}

 

CHouse::~CHouse(void)

{

delete &bed;

}

 

voidCHouse::GoToBed()

{

bed.Sleep();

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZC·Shou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值