条款 3:绝对不要以多态(polymorphically)方式处理数组
继承(inheritance)的最重要性质之一就是:你可以通过“指向 base class objects”的 pointers或 references,来操作 derived class objects。如此的 pointers和 references,我们说其行为是多态的(polymorphically)——犹如它们有多重类型似的。C++也允许你通过 base class的 pointers和 references 来操作“derived class objects 所形成的数组”。但这一点也不值得沾沾自喜,因为它几乎绝不会如你所预期般地运作。
举个例子,假设你有一个 class BST(意思是 binary search tree)及一个继承自 BST的 class BalancedBST:
在一个真正具规模的程序中,这样的 classes 可能会被设计为 templates,不过这不是此处重点;如果加上 template 各种语法,反而使程序更难阅读。针对目前的讨论,我假设 BST 和 BalancedBST 都只内含 ints。
现在考虑有个函数,用来打印 BSTs 数组中的每一个 BST 的内容:
当你将一个由 BST 对象组成的数组传给此函数,没问题:
然而如果你将一个 BalancedBST 对象所组成的数组交给 printBSTArray 函数,会发生什么事:
你的编译器会毫无怨言地接受它,但是看看这个循环(就是稍早出现的那一个)。
array[i] 其实是一个“指针算术表达式”的简写:它代表的其实是*(array+i)。我们知道,array 是个指针,指向数组起始处。array 所指内存和array+i 所指内存两者相距多远?答案是 i*sizeof(数组中的对象),因为array[0] 和 array[i] 之间有 i 个对象。为了让编译器所产生的代码能够正确走访整个数组,编译器必须有能力决定数组中的对象大小。很容易呀,参数 array 不是被声明为“类型为 BST”的数组吗?所以数组中的每个元素必然都是 BST 对象,所以 array 和 array+i 之间的距离一定是 i*sizeof(BST)。
至少你的编译器是这么想的。但如果你交给 printBSTArray 函数一个由BalancedBST 对象组成的数组,你的编译器就会被误导。这种情况下它仍假设数组中每一元素的大小是 BST 的大小,但其实每一元素的大小是 BalancedBST 的大小。由于 derived classes 通常比其 base classes 有更多的 data members,所以derived class objects 通常都比其 base class objects 来得大。因此,我们可以合理地预期一个 BalancedBST object 比一个 BST object 大。如果是这样,编译器为printBSTArray 函数所产生的指针算术表达式,对于 BalancedBST objects 所组成的数组而言就是错误的。至于会发生什么结果,不可预期。无论如何,结果不会令人愉快。
如果你尝试通过一个 base class 指针,删除一个由 derived class objects 组成的数组,那么上述问题还会以另一种不同面貌出现。下面是你可能做出的错误尝试:
//删除一个数组,但是首先记录一个有关此删除动作的消息。
虽然你没有看到,但其中一样有“指针算术表达式”的存在。是的,当数组被删除,数组中每一个元素的 destructor 都必须被调用(见条款 8),所以当编译器看到这样的句子:
必须产生出类似这样的代码:
//将 *array 中的对象以其构造顺序的相反顺序加以析构。
如果你这么写,便是一个行为错误的循环。编译器如果产生类似代码,当然同样是个行为错误的循环。C++语言规范中说,通过 base class 指针删除一个由derived classes objects构成的数组,其结果未定义。我们知道所谓“未定义”的意思就是:执行之后会产生苦恼。简单地说,多态(polymorphism)和指针算术不能混用。数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用。
注意,如果你避免让一个具体类(如本例之 BalancedBST)继承自另一个具体类(如本例之 BST),你就不太能够犯“以多态方式来处理数组”的错误。如条款33所说,设计你的软件使“具体类不要继承自另一个具体类”,可以带来许多好处。我鼓励你翻开条款 33,好好看看完整内容。