引子
经常大家都会碰到这样的问题,定义一个数据类,而此数据类型又包含此数据类型的变量。大家的通常做法应该都是把数据类型中的变量定义为指针形式。例如定义一个二叉树的节点。
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字节。所以,如果采用指针的形式,struct tagNode的大小在struct tagNode编译后即可确定。大小为sizeof(int)+4+4。对于对象形式的struct tagNode定义,其长度是在编译后无法确定的。
注意
- vs2010编译器中,错误C2460代表这种递归定义错误,编译提示:“identifier1”: 使用正在定义的“identifier2” ,将类或结构 (identifier2) 声明为其本身的成员(identifier1)。不允许类和结构的递归定义。
- 编程过程中,禁止类和结构的递归定义。以防产生奇怪的编译错误。
类的递归定义
现在,我们继续讨论类递归定义的经典案例—两个类相互包含引用问题。这个问题是所有C++程序员都会碰到的经典递归定义案例。
案例:CA类包含CB类的实例,而CB类也包含CA类的实例。代码实现如下:
// A.h 实现CA类的定义。
#include “B.h”
class CA
{
public:
int iData; // 定义int数据 iData
CB instanceB; // 定义CB类实例 instanceB
}
// B.h 实现CB类的定义。
#include “A.h”
class CB
{
public:
int iData; // 定义int数据 iData
CA instanceA; // 定义CA类实例 instanceA
}
int main()
{
CA instanceA;
return 0;
}
上述代码在VC2010上编译,代码无法编译通过,编译器会报出C2460错误。我们先分析一下代码存在的问题:
- CA类定义时使用CB类定义。CB类定义使用CA类的定义,递归定义。
- A.h包含了B.h,B.h包含了A.h,也存在递归包含的问题。
其实,无论是结构体的递归定义,还是类的递归定义。最后都归于一个问题,C/C++采用静态编译模型。程序运行时,结构或类大小都会在编译后确定。程序要正确编译,编译器必须知道一个结构或结构所占用的空间大小。否则编译器就会报出奇怪的编译错误,如C2460错误。
最后,我们给出类或结构体递归定义的几个经典解法,这儿以类为例。因为在C++中struct也是class,class举例具有通用性。
前向声明实现
// A.h 实现CA类的定义。
class CB; // 前向声明CB类
class CA
{
public:
int iData; // 定义int数据 iData
CB *pinstanceB; // 定义CB类的指针p instanceB
}
// B.h 实现CB类的定义。
#include “A.h”
class CB
{
public:
int iData; // 定义int数据 iData
CA instanceA; // 定义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类型的指针,而不能实例化。
friend声明实现
// A.h 实现CA类的定义。
class CA
{
public:
friend class CB; // 友元类声明
int iData; // 定义int数据 iData
CB *pinstanceB; // 定义CB类的指针p instanceB
}
// B.h 实现CB类的定义。
#include “A.h”
class CB
{
public:
int iData; // 定义int数据 iData
CA instanceA; // 定义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友元实现,有一点是肯定的:最多只能一个类可以定义实例。同样头文件包含也是一件很麻烦的事情,再加上头文件中常常出现的宏定义。而且各种宏定义的展开是非常耗时间的。类或结构体递归定义实现应遵循两条原则:
- 如果可以不包含头文件,那就不要包含了。这时候前置声明可以解决问题。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象(非指针),也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知。
- 尽量在CPP文件中包含头文件,而非在头文件中。假设类A的一个成员是是一个指向类B的指针,在类A的头文件中使用了类B的前置声明并便宜成功,那么在A的实现中我们需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类B的头文件而非声明部分。
请谨记
- 类和结构体定义中禁止递归定义,以防产生奇怪的编译错误。
- 如果两个类相互递归定义时,需考虑前向声明或friend友元实现。但无论通过何种方法实现,有一点是肯定的:最多只能一个类可以定义实例。