大家经常会碰到这样的问题:定义一个数据类型,而此数据类型又包含此数据类型的变量。通常做法应该是把数据类型中的变量定义为指针形式。例如定义一个二叉树的节点:
struct tagNode //二叉树节点
{
ElemType data; //节点数据
struct tagNode *pLeftNode; //节点左孩子
struct tagNode *pRightNode; //节点右孩子
}
上述二叉树节点定义是正确的,可以编译通过。但如果定义为下述对象形式,则无法编译通过:
struct tagNode //二叉树节点
{
ElemType data; //节点数据
struct tagNode LeftNode; //节点左孩子
struct tagNode RightNode; //节点右孩子
}
然而上述对象形式的二叉树节点为何不能编译通过,而采用指针形式的二叉树节点可以编译通过呢?
答案是 C/C++采用静态编译模型。程序运行时,结构大小会在编译后确定。程序编LeftNode.LeftNode.LeftNode..-.这类错误也称为类和结构的递归定义错误。要正确编译,编译器必须知道一个结构所占用的空间大小。除此之外还有一个逻辑方面的问题,在这种情况下,想想可以有LeftNode.LeftNode.LeftNode.LeftNode.
如果采用指针形式,指针的大小与机器的字长有关,不管是什么类型,编译后指针的大小总是确定的,所以这种情况下不需要知道结构 struct tagNode 的确切定义。例如,在32 位字长的 CPU 中,指针的长度为 4 字节。所以,如果采用指针的形式,structtagNode的大小在其编译后即可确定,为sizeof(int)+4+4.对于对象形式的 struct tagNode定义,其长度在编译后是无法确定的。
注意:
(1) 在Visual studio 2010编译器中,错误C2460代表这种递归定义错误,编译提示:”identifier1″:使用正在定义的”identifier2″,将类或结构(identifier2)声明为其本身的成员(identifier1),不允许类和结构的递归定义。
(2) 在编程过程中,禁止类和结构的递归定义,以防产生奇怪的编译错误。现在,我们继续讨论类递归定义的经典案例一两个类相互包含引用问题。这是所有 C++程序员都会碰到的经典递归定义案例。
案例:CA 类包含 CB类的实例,而 CB类也包含 CA 类的实例。代码实现如下:
//A.h实现CA类的定义
#include"B.h"
class CA
{
Public:
intiData; //定义int数据iData
CB instanceB: //定义CB 类实例instanceB
}
//B.h实现CB类的定义
#include"A.h"
class CB
{
public:
int iData; //定义int数据iData
CAinstanceA; //定义CA 类实例instanceA
}
int main()
{
CAinstanceA;
return 0;
}
上述代码在VisualC 2010上无法编译通过,编译器会报出 C2460 错误。先分析一下代码存在的问题:
● CA类定义时使用CB类的定义,CB类定义时使用CA类的定义,递归定义。
● A.h包含了B.h,B.h 包含了A.h,也存在递归包含的问题。
其实,无论是结构体的递归定义,还是类的递归定义,最后都归于一个问题C/C++采用静态编译模型。在程序运行时,结构或类大小都会在编译后确定。程序要正确编译,编译器必须知道一个结构或结构所占用的空间大小,否则编译器就会报出奇怪的编译错误,如C2460错误。
最后,我们给出类或结构体递归定义的几个经典解法。这里以类为例,因为在C++中 struct也是class,class举例具有通用性。
1.前向声明实现
//A.h实现CA类的定义
class CB;//前向声明CB类
class CA
{
public:
Int iData; //定义int数据iData
CB*pinstanceB; //定义 CB类的指针 pinstanceB
}
//B.h实现CB类的定义
#include "A.h"
class CB
{
public:
Int iData; //定义int数据iData
CAinstanceA; //定义 CA类实例instanceA
}
#include"B.h"
int main()
{
CA instanceA;
return 0;
}
前向声明实现方式的主要实现原则:
●主函数只需要包含 B.h就可以,因为 B.h 中包含了A.h。
●A.h 中不需要包含 B.h,但要声明 class CB,在避免死循环的同时也成功引用了CB。
●包含class CB 声明,而没有包含头文件 B.h,这样只能声明 CB类型的指针,而不能实例化。
2.friend声明实现
//A.h实现CA类的定义
class CA
{
public:
friend class CB; /友元类声明
int iData; //定义int数据iData
CB *pinstanceB; //定义CB类的指针 pinstanceB
}
//B.h实现CB类的定义
#include "A.h"
class CB
{
public:
int iData; //定义int数据iData
CAinstanceA; //定义 CA类实例 instanceA
}
#include"B.h"
int main()
{
CA instanceA;
return 0:
}
friend友元声明实现说明:
● 主函数只需要包含B.h就可以,因为B.h中包含了A.h。
● A.h 中不需要包含B.h,但要声明class CB,在避免死循环的同时也成功引用了CB。
● class CA 包含class CB友元声明,而没有包含头文件 B.h,这样只能声明 CB类型的指针,而不能实例化。
不论是前向声明实现还是friend友元实现,有一点是肯定的:即最多只能有一个类可以定义实例。同样头文件包含也是一件很麻烦的事情,再加上头文件中常常出现的宏定义,感觉各种宏定义的展开是非常耗时间的。类或结构体递归定义实现应遵循两个原则:
(1) 如果可以不包含头文件,那就不要包含了。这时候前置声明可以解决问题。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象(非指针),也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知。
(2) 尽量在CPP文件中包含头文件,而非在头文件中。假设类A的一个成员是一个指向类B的指针,在类A的头文件中使用了类B的前置声明并编译成功,那么在A的实现中需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类 B的头文件而非声明部分。
请谨记
● 类和结构体定义中禁止递归定义,以防产生奇怪的编译错误。如果两个类相互递归定义时,需考虑前向声明或friend友元实现。但无论通过何种方法实现,有一点是肯定的:即最多只能有一个类可以定义实例。