C++ 前置声明 和 包含头文件 如何选择

假设有一个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是由其所容纳的值所确定的,所以在看到所有的值之前,编译器是无法确定其存储的
原文地址:https://www.cnblogs.com/rednodel/p/5000602.html