在编写C++程序的时候,经常需要用到前置声明(Forward declaration)。其中一种情况就是,有类A和类B(如下代码所示),在A中要用到B中的成员(变量\函数),然后在B中也要A中的成员(变量\函数),用前置声明可以解决这种相互包含的问题,而用相互#include头文件的方式会导致编译错误,为什么?
//a.h
class B; // 前置声明(Forward declaration)
class A
{
private:
//B b; // 会出错
B* b; // 前置声明只能作为指针或引用(因为引用也是居于指针实现),不能定义类的对象,为什么‚
public:
A(B* b):b(b){} // 如果将类A的成员变量B* b;改写成B& b;的话,必须要将b在A类的构造函数中,采用初始化列表的方式初始化,否则会出错
Void someMethodA()
{
//b->someMethodB();// 在这里实现的话,会编译错误,那为什么呢?ƒ
}
…
};
//b.h
class A;// 前置声明(Forward declaration)
class B
{
private:
A* a;
public:
Void someMethodA()
{
cout << "something happened..." << endl;
}
…
};
问题一:用相互#include头文件的方式会导致编译时编译器陷入死循环,而是用前置声明,注意,前置声明的重点还是“声明”两个字,那个编译器就只用知道有这个被前置声明了的类的存在,而不用去编译该类。
问题二:那是因为指针是固定大小,并且可以表示任意的类型。而改回答没有完全回答到。请看:
class B
{
private:
Int _b; //new add
Int _a;
public:
B(int a):_a(a),_b(a){} //_b is new add
Int get_a() const {return _a;}
Int get_b() const {return _b;} //new add
};
我们看上面定义的这个类B,其中_b变量和get_b()函数是新增加进这个类的。
那么我问你,在增加进_b变量和get_b()成员函数后这个类发生了什么改变,思考一下再回答。
好了,我们来列举这些改变:
第一个改变当然是增加了_b变量和get_b()成员函数;
第二个改变是这个类的大小改变了,原来是4,现在是8。
第三个改变是成员_a的偏移地址改变了,原来相对于类的偏移是0,现在是4了。
第四个隐藏的改变是类A的默认构造函数和默认拷贝构造函数发生了改变(最重要的改变)。
由上面的改变可以看到,任何调用类A的成员变量或成员函数的行为都需要改变,我们的b.h也需要重新编译。
如果我们的a.h是这样的:
//a.h
#include “b.h”
class A
{
private:
B b;
};
那么我们的a.h也需要重新编译。代码中的语句:B a;是需要了解B的大小的,不然无法给类A分配内存大小,因此不完整的前置声明就不行,必须要包含b.h来获得类B的大小,同时也要重新编译类A。
如果是这样的:
//a.h
class B
class A
{
private:
B *b;
};
那么我们的a.h就不需要重新编译。像这样前置声明类B:class B;
是一种不完整的声明,只要类A中没有执行需要了解类A的大小或者成员的操作,则这样的不完整声明允许声明指向A的指针和引用。
使用前置声明只允许的声明是指针或引用的一个原因是只要这个声明没有执行需要了解类B的大小或者成员的操作就可以了,所以声明成指针或引用是没有执行需要了解类B的大小或者成员的操作的。也即如果执行了要了解类B的大小或者成员的操作的,则必须#include “b.h”,(这里就回答了问题三:因为使用了类型B的定义,调用了类B中的一个成员函数。前置声明class B;仅仅声明了有一个B这样的类型,并没有给出相关的定义,因此出现了编译错误。解决办法是将类的声明和类的实现(即类的定义)分离)
类的前置声明是有许多的好处的。
一:当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。
二:减小类A的大小,上面的代码没有体现,那么我们来看下:
//a.h
class B
class A
{
private:
B* b;
};
//b.h
class B
{
private:
Int a;
Int b;
Int c;
};
我们看上面的代码,类B的大小是12(在32位机子上)。
如果我们在类A中包含的是B的对象,那么类A的大小就是12(假设没有其它成员变量和虚函数)。如果包含的是类B的指针*b变量,那么类A的大小就是4,所以这样是可以减少类A的大小的,特别是对于在STL的容器里包含的是类的对象而不是指针的时候,这个就特别有用了。