假设有一个Date类

Date.h

class Date {
private:
int year, month, day;
};

如果有个Task类的定义要用到Date类,有两种写法

其一

Task1.h

class Date;
class Task1 {
public:
Date getData();
};

其二

Task2.h

#include "Date.h"
class Task2 {
public:
Date getData();
};

一个采用前置声明,一个采用#include<Date.h>加入了Date的定义。两种方法都能通过编译。但是 Task1.h 这种写法更好。如果Date.h 的 private 成员变量改变,比如变成 double year, month, day; ,Task1.h 不需要重新编译,而 Task2.h 就要重新编译,更糟的是如果 Task2.h 还与其他很多头文件有依赖关系,就会引发一连串的重新编译,花费极大的时间。可是事实上改变一下写法就可以省去很多功夫。

所以能用前置声明代替#include 的时候,尽量用前置声明

有些情况不能用前置声明代替#include

比如Task1.h改成

class Date;
class Task1 {
public:
Date d;
};

会编译错误,因为Date d定义了一个Date类型变量,编译器为d分配内存空间的时候必须知道d的大小,必须包含定义Date类的Date.h文件。

这是可以采用指针来代替

class Date;
class Task1 {
public:
Date *d;
};

指针的大小是固定的。在32位机上是4字节,64位机上是8字节。这时编译Task1的时候不需要Date的大小,所以和Date的定义无关。

何时可以用前置声明代替#include

在这里,我自己总结了可以使用前置声明来取代包括头文件的各种情况和给出一些示例代码。

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

假设我们有类型A和类型C,在哪些情况下在A需要C的定义:

  1. A继承至C
  2. A有一个类型为C的成员变量
  3. A有一个类型为C的指针的成员变量
  4. A有一个类型为C的引用的成员变量
  5. A有一个类型为std::list<C>的成员变量
  6. A有一个函数,它的签名中参数和返回值都是类型C
  7. A有一个函数,它的签名中参数和返回值都是类型C,它调用了C的某个函数,代码在头文件中

1,没有任何办法,必须要获得C的定义,因为我们必须要知道C的成员变量,成员函数。

2,需要C的定义,因为我们要知道C的大小来确定A的大小,但是可以使用Pimpl惯用法来改善这一点,详情请
看Hurb的Exceptional C++。

3,4,不需要,前置声明就可以了,其实3和4是一样的,引用在物理上也是一个指针,它的大小根据平台不同,可能是32位也可能是64位,反正我们不需要知道C的定义就可以确定这个成员变量的大小。

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

6,不需要,只要我们没有使用到C。

7,需要,我们需要知道调用函数的签名。

上述例子可以说明

如果使用object reference 或 object point 可以完成任务,就不要用object

这样可以尽最大可能避免#include

 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.h>,而B中又有A类型的成员变量也需要include<a.h>,这就导致了循环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:一个只提供接口,另一个负责实现!
 
C++标准其实是不支持enum的前置声明的,原因在于标准规定Enum的size是由其所容纳的值所确定的,所以在看到所有的值之前,编译器是无法确定其存储的

C++ 前置声明 和 包含头文件 如何选择的更多相关文章

  1. ZT 头文件包含其实是一想很烦琐的工作 第一个原则应该是,如果可以不包含头文件

    当出现访问类的函数或者需要确定类大小的时候,才需要用头文件(使用其类定义)    http://blog.csdn.net/clever101/article/details/4751717 看到这个 ...

  2. C++中#include包含头文件带 .h 和不带 .h 的区别

    C++中#include包含头文件带 .h 和不带 .h 的区别? 如 #include <iostream> 和 #include <iostream.h> 包含的东西有哪些 ...

  3. 在.h和.cpp中包含头文件的区别

    1.在.h中包含头文件,是为了声明一系列这个头文件的变量等,可能会产生重复包含的问题: 2.在.cpp中包含头文件只是为了实现这个头文件或者使用其中的方法,不会有重复包含的问题,所以尽量在源文件中包含 ...

  4. include包含头文件的语句中,双引号和尖括号的区别是什么?

    include包含头文件的语句中,双引号和尖括号的区别是什么?  #include <> 格式:引用标准库头文件,编译器从标准库目录开始搜索 尖括号表示只在系统默认目录或者括号内的路径查找 ...

  5. C/C++不同文件夹下包含头文件的方法及#include的使用

    转自:http://blog.sina.com.cn/s/blog_6e0693f70100so42.html 本文主要介绍了如何不同文件夹下使用预处理器指示符#include. 假设我们有如下一个工 ...

  6. include包含头文件的语句中,双引号和尖括号的区别

    include包含头文件的语句中,双引号和尖括号的区别   #include <>格式:引用标准库头文件,编译器从标准库目录开始搜索 #incluce ""格式:引用非 ...

  7. c++包含头文件好还是重新定义好

    A.h struct A { int a; int b; }; B.cpp 在B.cpp里面用到这个结构体 有两种方法 .自己定义一个一模一样的结构体 struct A { }; .包含A.h头文件 ...

  8. C++包含头文件时尖括号和双引号区别

    原文链接:http://c.biancheng.net/cpp/biancheng/view/66.html 如果你还看一些别的C++教程,那么你可能很早就发现了,有些书上的#include命令写作# ...

  9. C++ 包含头文件 和 宏的使用 和 条件编译

    1 #define命令剖析 1.1   #define的概念     #define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本. ...

随机推荐

  1. label的for属性与inputde的id元素绑定

    <form> <label for="male">Male</label> <input type="radio" n ...

  2. thinkphp使用模块/控制器/操作访问时出现No input file specified.解决方式

    thinkphp使用 http://serverName/index.php/模块/控制器/操作 访问时,出现了 No input file specified. 的错误 解决办法: 一: 开启cgi ...

  3. 利用cookies获取登录后的网页

    众所周知,HTTP连接是无状态的,那么问题来了,怎么记录用户的登录信息呢?通常的做法是用户第一次发送HTTP请求时,在HTTP Server端生成一个SessionID,SessionID会对应每个会 ...

  4. Wpf中MediaElement循环播放

    原文:Wpf中MediaElement循环播放 前一段时间做了一个项目,里面牵涉到媒体文件的循环播放问题,在网上看了好多例子,都是在xaml中添加为MediaElement添加一个TimeLine,不 ...

  5. SSRS 请求并显示SharePoint人员和组字段

    场景: 使用Reporting Service请求SharePoint List,该list中包含人员和组字段.要求:只显示人员或组的display name.示例如下: 项目 参与人员 期望显示 项 ...

  6. Android自定义带标题边框的Layout

    今天工作中又碰到个小问题,项目需要用到像Java Swing的JPanel一样带标题边框的布局,Android里没有类似控件,想到这个也不难,自己画了一个,是继承LinearLayout的一个自定义布 ...

  7. Jquery ui datepicker 设置日期范围,如只能隔3天

    最近的后台项目前端使用了jquery ui 日历控件自然就使用了jquery ui 的   datepicker 后台数据比较好大,一般是千万级的和百万级的关联,查询会很慢,所以后加想多加些过滤条件, ...

  8. android——fragment长时间home或者锁屏java.lang.IllegalArgumentException:No view found for id for....

    在项目中遇到了这个问题.头痛了很久,总是无法重现,也不能很好的解决.总是在息屏后一段时间,就会报java.lang.IllegalArgumentException:No view found for ...

  9. oc特有语法

    分类 问题 1.什么是分类? 就是把一个类的功能,分出一部分来放在一个独立的文件中 2.分类的语法是什么样的? @interface Person(SuperMan) 3.分类与类是什么关系? 分类依 ...

  10. 封装一些数据库SQLCipher的方法(增、删、改、查)

    上一篇随笔只是简单的说了一下使用SQLCipher框架,介绍的比较笼统,可能看一遍之后更加蒙圈了,为了更好的使用这个数据库,整理了我在公司项目的需要用的方法,包括创建表,插入数据,更新数据,搜索查询数 ...