实用经验 11 为何struct x1{struct x1 stX};无法通过编译?

引子

经常大家都会碰到这样的问题,定义一个数据类,而此数据类型又包含此数据类型的变量。大家的通常做法应该都是把数据类型中的变量定义为指针形式。例如定义一个二叉树的节点。

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友元实现。但无论通过何种方法实现,有一点是肯定的:最多只能一个类可以定义实例。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值