9.4 重复子对象

当从某个基类继承时,可以在其派生类中得到那个基类的所有数据成员的副本。下面的程序说明了多个基类子对象在内存中的可能布局:[1]

9.4 重复子对象 - 图1

正如读者所见,对象c的B子对象部分从整个对象开始位置偏移了4个字节。其布局如下所示:

9.4 重复子对象 - 图2

对象c以它的A子对象作为开头,然后是B子对象部分,最后的数据完全来自类型C本身。因为C“is-an”[2]A并且也“is-a”B,所以它可以向上类型转换为两者之中任一基类型。当向上类型转换为A时,结果指针指向A子对象部分,这发生在C对象的开始位置,所以地址ap等同于表达式&c。然而,当向上类型转换为B子对象时,结果指针必须指向B子对象所在的实际位置,因为类B并不知道有关类C(或类A,就本例而言)的事情。换句话说,被bp指向的对象必须能够产生和独立的B对象相同的行为(除了任何需要多态的行为以外)。

当对bp进行类型转换倒退为一个C时,由于原始对象是C, bp指向该对象开始的位置,因为它已经知道B子对象的位置,所以指针被调整指向了完整对象的起始地址。如果bp指向的是一个独立的B对象而不是C对象的开始位置,那么这种类型转换就是不合法的。[3]此外,在比较表达式bp==cp中,cp被隐式转换为B,这是使该比较表达式变得有意义的惟一方法(这就是说,向上类型转换总是允许的),因此结果是true。因此,当在子对象和完整类型间来回转换时,要应用适当的偏移量。

空指针需要进行特别的处理。显然,如果在开始进行类型转换时指针为零,那么在转换到一个B子对象或从一个B子对象转换回来时,由于盲目地减去偏移量将会导致产生无效的地址。基于这种原因,当类型转换到B或有来自B的类型转换时,编译器产生逻辑检查,首先查看该指针是否为零。如果不为零,则可以应用偏移量;否则,当指针为零时就放弃使用偏移量。

根据目前学习到的语法,如果现在有多个基类,并且如果这些基类依次有一个共同的基类,那么将得到顶层基类的两个副本。如下面的例子所示:

9.4 重复子对象 - 图3

因为对象b的长度是20个字节,[4]所以在一个完整的Bottom对象中共有5个整型变量。这种情况的典型类图通常如下所示:

9.4 重复子对象 - 图4

这就是所谓的“菱形继承”(也称“钻石继承”),但是在这个例子中可以较好地表示为如下的类图:

9.4 重复子对象 - 图5

这种设计的不足之处表现在前面代码中Bottom类的构造函数上。用户认为只需要4个整型变量,但是哪些实际参数才是传递给Left和Right所需要的两个参数呢?尽管这一设计并不是固有的“错误”,但通常它并不是一个应用程序所需要的。在尝试将指向Bottom对象的指针转换成指向Top的指针时,同样也会出现问题。如前所述,可能需要调整对象指针的地址,这依赖于在完整的对象内部各子对象所处的位置,但是这里却有两个Top子对象供选择。因为编译器不知道选择哪一个,所以这样一种向上类型转换是模棱两可的(二义性),也是不允许的。用同样的原因可以解释为什么一个Bottom对象不能调用那个只定义在Top中的函数。如果存在这样一个函数Top:f(),那么调用b.f()需要涉及一个Top子对象作为执行语境,而这里却有两个Top可供选择。

[1]实际的布局在实现时确定。

[2]“我们常把基类和派生类之间的关系看做是一个‘is-a’(是)关系”。见《C++编程思想第1卷:标准C++导引》第1章。—编辑注

[3]但并不作为一个错误被检查。dynamic_cast可以解决这个问题。看前面章节的详细说明。

[4]即5*sizeof(int)。因为编译器可以加入任意的数据类型,所以对象的长度至少是它各部分的总和,也可以更长。