【看到这篇文章,肯定有疑问,为什么要使用前置声明】?
让我们先来看一段代码:
//date.h
#include<iostream>
using namespace std;
#include"time.h"
class Date
{
public:
Date(int year,int month,int day);
Date(const Date&d);
~Date();
void fun();
private:
int _year;
int _month;
int _day;
Time t;
};
//time.h
#include<iostream>
using namespace std;
#include"date.h"
class Time
{
public:
Time(int hour,int minute,int second);
Time(const Time& t);
~Time();
int Gethour();
private:
int _hour;
int _minute;
int _second;
Date d;
};
在上面的两段代码中,第一个是日期类的声明,第二个是时间类的声明,在日期类中要定义一个时间类的对象,那么就要引用下时间类的头文件,在时间类中要定义一个日期类的对象,就要在时间类里引用下日期类的头文件,看时没什么大的毛病,程序一走,结果如下:
深度这么大!这是什么鬼!我们不要惊讶,来分析下,到底怎么回事。
这段代码说白了就是嵌套定义,你中有我,我中有你,这样我找你的同时,你也在找我,这样无线递归下去,深度就很大了。这时,人类总是很聪明的,引入了前置声明。
//date.h
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day);
Date(const Date&d);
~Date();
void fun();
private:
int _year;
int _month;
int _day;
};
//time.h
#include<iostream>
using namespace std;
class Date;
class Time
{
public:
Time(int hour,int minute,int second);
Time(const Time& t);
~Time();
int Gethour();
private:
int _hour;
int _minute;
int _second;
Date *d;
};
<pre name="code" class="cpp">//time.cpp
#include"time.h"
#include"date.h"
Time::Time(int hour,int minute,int second)
{
_hour = hour;
_minute = minute;
_second = second;
}
Time::Time(const Time &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
int Time::Gethour()
{
d->fun();
return _hour;
}
Time::~Time()
{}
//date.cpp
#include"date.h"
Date::Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date::Date(const Date &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Date::fun()
{}
Date::~Date()
{}
就这样添加了一个class的类的声明就解决问题了!!!怎么解决的,我相信读者现在还是一头雾水,别着急,接下来咱们来稍稍深入的探究前置声明:
关于前置声明的好处多多,以前面的例子来说:
第一:当我们在类time中使用类date的声明时,如果我们要修改类date的东西,那么这时只用编译类date,就可以了,而不用编译类time,这样也提高了程序的运行效率。
第二:减小了类的大小,time 中定义了一个Date类型的指针变量,如果不是这样,那么time类中的大小就会是它原本的12(三个数据成员)加上date类中的三个,一共24,而用指针就减少了很多,变成了12+4=16。
注意:这第二点也是我要强调的很重要的一点,在创建对象的时候要使用指针。
//a.h
#include "b.h"
class A
{
....
private:
B b;
};
class A
{
public:
A(int a):_a(a),_b(a){}
int Get_a() const {return _a;}
int Get_b() const {return _b;}
private:
int _b; // new add
int _a;
};
其中变量_b和函数Get_b()是新加入类A中的,那么如果现在问你,在增加了这两个东西后类A发生了哪些变化,请谨慎的思考然后回答。
很显而易见的,你肯定会说:
1)类A的大小变了,原来是4,现在是8
2)类A的对象_a的偏移地址发生了变化,原来相对于类A的偏移地址是0,现在是4
还有吗?再仔细的想想,还有哪个隐蔽的不容易发现的变化。
对的就是类A的默认构造函数和默认的拷贝构造函数也发生了变化。
由上面的改变可以看到,任何调用类A的成员变量或成员函数的行为都需要改变,因此,我们的a.h需要重新编译。
如果我们的b.h是这样的:
//b.h
#include "a.h"
class B
{
...
private:
A a;
};
那么我们的b.h也是需要重新编译的,但是如果我们的b.h是这样的:
class A;
class B
{
...
private:
A *a;
};
就不需要编译了。
我们这样前置声明类A:class A;是一种不完整的声明,只要类B中没有执行需要了解类A的大小或者成员的操作,则这样的不完整声明允许声明指向A的指针和引用。而在前一个代码中的语句A a;是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含a.h来获得类A的大小,同时也要重新编译类B。再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是只要这个声明没有执行需要了解类A的大小或者成员的操作就可以了,所以声明成指针或引用是没有执行需要了解类A的大小或者成员的操作的。
【再次总结 】 :
1、对前置声明:
1)降低模块的耦合。因为隐藏了类的实现,被隐藏的类相当于原类不可见,对隐藏的类进行修改,不需要重新编译原类。
2)降低编译依赖,提高编译速度。指针的大小为(32位)或8(64位),X发生变化,指针大小却不会改变,文件c.h也不需要重编译。
3)接口与实现分离,提高接口的稳定性。
1、通过指针封装,当定义“new C”或"C c1"时 ,编译器生成的代码中不会掺杂X的任何信息。
2、当使用C时,使用的是C的接口(C接口里面操作的类其实是pImpl成员指向的X对象),与X无关,X被通过指针封装彻底的与实现分离。