当前位置:C++技术网 > 资讯 > C++中类定义时不允许使用不完整的类型错误提示原因分析

C++中类定义时不允许使用不完整的类型错误提示原因分析

更新时间:2016-02-04 19:58:15浏览次数:1+次

    在C++中定义一个类,同时在类中定义此类的对象作为类的组成部分。这个情况确实需要,然后你会发现编译器提示“不允许使用不完整的类型”错误提示。在文章《C++“不允许使用不完整类型”的解决方案》中,已经说明了如何解决此错误。
    不过为了探究此错误底下的更多的信息,让初学者更明白为什么错了,对于学习更有好处。知道为什么错了,就不会再无意中犯错。当你深入了解了之后,会让你的思想产生震撼。因为错误的背后,包含了一个思想的碰撞。只是你的理解和人家设计的不一样,当你完全知道人家设计的思想,你也就知道为什么会犯错,为什么语法要这样。
    不多说,开始进入正题。C++中类定义时不允许使用不完整的类型会在下面的代码中出现:
class A
{
    int i;
    float b;
    A aa;//定义类对象
    double c;
}
    在这个简单A类声明中,却又不平凡的味道,因为,报错啦!这是硝烟的味道。为什么有硝烟,那是因为思维开始发生碰撞。你从你的直觉思维里,看不出这个代码有什么问题,然而编译器就始终提示“不允许使用不完整的类型”。你一定会再三检查,还检查不出问题,然后开始怀疑VS是不是有Bug。你可能会创建一个新的项目测试,会发现没有问题。如果一直都找不到答案,你会比较苦恼,甚至开始怀疑人生!天啊,我连类定义都搞不好,还能学会编程吗?然后留下心理阴影。有些人因为一些问题,就放弃了编程。编程是需要内心非常强大的人的。如果你还是程序员,或者你要成为程序员,那都是很让人佩服的事情。
    就我而言,再没有搞清楚这个问题之前,也是苦恼不已,只是暂时放下了,然后继续学习。然后在不断地学习过程中,慢慢顿悟了。那是因为各种知识点蕴含的思想在脑子里碰撞,让你的脑子思考的越多,然后慢慢就能够将学到的东西前后联系起来,能够联想对比触类旁通了。这就是所谓的找到了编程的感觉。

    为了说明这个问题,我们先来看一张图:

C++中类定义时不允许使用不完整的类型错误提示原因分析图

【C++中类定义时不允许使用不完整的类型错误提示原因分析图】
    从图中我们可以看到,实际上图中显示的是两个问题呢。我们先来看看类定义时不允许使用不完整的类型错误。类是什么?类简单来看,就是一个类型而已。我们通常的int类型,都是基本的类型,类型的定义都是系统写好了的。然而,为了支持更多的自定义类型,引入了结构体,在C++中则更进一步,引入了类,也就引进了面向对象的思想。
    所以,类只是让你自己定制类型的东西。你定制好后,这个类型就可以用来定义对象变量了。而定制类类型的本质,就是决定类对象的内存布局和访问控制方式。内存布局就是成员变量以及成员函数的声明顺序。而控制方式则是各种基本类型和函数的访问,用什么权限访问等等。
    所以,类中不同的声明顺序和权限控制(私有、保护和公开),会导致类对象的内存布局和操作方式不一样哦。当然,声明的变量的多少也决定类类型是哪样的。当然,一个完整的类类型的定义,是从左花括号开始的,右花括号结束。中间的任何声明都是决定类类型的因素。变量多大、类型如何、声明顺序,访问方式,变量和函数的个数等,都是觉得类类型的因素。
    如果我们在类声明中用类来定义一个对象变量作为成员变量,问题就来了。因为本次定义就是类的类型的一部分。也就是说,当你在用自己这个类定义一个对象时,你正在声明自己这个类型。当编译器执行到:
A a;
    时,A类并没有被声明完毕。此时用这个没有声明完毕的A类去定义对象a,编译器无法确定a应该占用多大的内存。因为在a对象定义之后,后面还有c变量的定义哦。不管有没有,在逻辑上说不过去。因为编译器并没有限制,你可以在后面加也可以不加。就算后面什么也没有,a自己占的内存也不确定,导致A类需要占用的内存大小就是未知数。
    而这一切的原因都是A类没有声明完毕就用来定义对象a了,就出现了这个怪现象。所以,编译器就提示了“不允许使用不完整的类型”错误信息。
    要说不完整类型的理解,到此也有个大概了。不完整的类型来源就是,在定义对象a之后,你不知道后面还有没有定义其他东西。如果定义,类所占的内存就变大。这个还是好理解的地方。我们下面要重点说明更加纠结的一点,就是a对象的定义,出现了递归的定义,就让A类所占的内存无限变大。

    下面是确定类中a对象的大小的过程分析:
    1.编译器是从上往下执行的。当执行到a对象的定义时,编译器发现a用A类定义的对象,所以编译器去找A类来确定a对象所占的内存大小。
    2.为了确定第一步对应的A类的a对象的内存的大小,就要确定A类的大小。所以,编译器来计算A类的大小,依次的遍历A类声明中的东西,然后又发现了在A类定义了对象a。而这个a的大小是多少呢?不知道,需要遍历A类才能知道。
    3.在第二步中要确定a的大小,就又遍历A类,然后会发现还有一个a的定义,所以就不停的遍历A类。停不下来。

    这就是为了确定对象a的内存大小不断的展开A类,然后又不停的确定A类中a对象的大小。其实这个就是一个死递归,没有一个终止条件。这样也就让编译器无法得知A类的完整信息。
    美国好莱坞大片中有很多电影都是这样的思想,都会让你看的精神崩溃,属于时空漏洞,出现很多个自己,穿越时空出现很多个自己,典型的电影为《恐怖游轮》。不过很多脑子不够用的人,就此蒙掉了,根本就看不懂。看的懂一点的,吓的要死,心理都受不了了,感觉就停不下来了。确实是停不下来。
    然而聪明的程序员,早已看穿了一切。不就是死循环和死递归这些类型嘛!哈哈哈。要说那些都是烧脑子的电影,对于程序员来讲,都是家常便饭了。如果你没有看过这些电影,可以去看看。
    所以最终的关键理解就在于,只有在最后一个花括号,才表示类的声明完毕了,才能用来定义对象。所以,你在类外定义对象,类自然都是完整定义好了的。而在类声明中就定义本类的对象,无论放在类的哪里,都是类类型声明中的一部分。你要理解完整的类定义是哪样的,还要知道在类中定义对象这个动作就是类类型本身的一部分。对象不定义好,类就不完整,类就不能用来定义对象。而想要完整的类,就要这个对象被定义好。这就是一个根本性矛盾。这在计算机中,叫做死锁,在哲学中叫做矛盾。

    而编译器选择要完整的类类型,所以不让定义对象,优先权在类的定义。这个和鸡生蛋蛋生鸡很类似。

    如果确实要在类中定义类对象,我们可以换一种方式实现,也就是用类指针。不管类有没有声明完毕,指针的内存都是可以确定的,所以这样就可以解决这个问题。这个在文章开始提到的文章中已经有介绍了。就不赘述了。