第六章,杂项讨论
33-将非尾端类(non-leaf classes)设计为抽象类
继承具体类导致的”赋值(切片)与异型赋值问题:”
以下,liz1只有Animal成分被修改和liz2相同
class Animal { public: Animal& operator=(const Animal& rhs); ... };
class Lizard: public Animal { public: Lizard& operator=(const Lizard& rhs); ... };
class Chicken: public Animal { public: Chicken& operator=(const Chicken& rhs); ... };
Lizard liz1; Lizard liz2;
Animal *pAnimal1 = &liz1; Animal *pAnimal2 = &liz2;
... *pAnimal1 = *pAnimal2;
|
解决办法1: 将assignment成为虚函数
如果Animal::operator=是虚函数,之前代码调用的就会是Lizard的assignment操作符
class Animal{ public: virtual Animal& operator=(const Animal& rgs); ... };
class Lizard: public Animal{ public: virtual Lizard& operator=(const Animal& rhs); };
class Chicken: public Animal{ public: virtual Chicken& operator=(const Animal& rhs); };
Lizard liz; Chicken chick;
Animal *pAnimal1 = &liz; Animal *pAnimal2 = &chicken; ... *pAnimal1 = *pAnimal2;
Lizard& Lizard::operator=(const Animal& rhs) { const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
proceed with a normal assignment of rhs_liz to *this; }
Lizard liz1; Chicken chicken;
Animal& pAnimal1 = lizard; Lizard lizard_animal1 = dynamic_cast<Lizard&>(pAnimal1);
Animal& pAnimal2 = chicken; Lizard lizard_animal2 = dynamic_cast<Lizard&>(pAnimal2);
class Lizard: public Animal{ public: virtual Lizard& operator=(const Animal& rhs); Lizard& operator=(const Lizard& rhs); ... };
Lizard liz1, liz2; ... liz1=liz2;
Animal *pAnimal1 = &liz1; Animal *pAnimal2 = &liz2; ... *pAnimal1=*pAnimal2;
Lizrd& Lizard::operator=(const Animal& rhs) { return operator=(dynamic_cast<const Lizard&>(rhs)); }
|
解决办法2: 将assignment成为Animal的private函数
class Animal { private: Animal& operator=(const Animal& rhs); ... };
class Lizard: public Animal { public: Lizard& operator=(const Lizard& rhs); ... };
class Chicken: public Animal { public: Chicken& operator=(const Chicken& rhs); ... };
Lizard liz1, liz2; ... liz1 = liz2;
Chicken chick1, chick2; ... chick1 = chick2;
Animal *pAnimal1 = &liz1; Animal *pAnimal2 = &chick1; ... *pAnimal1 = *pAnimal2;
Animal animal1, animal2; ... animal1 = animal2;
Lizard& Lizard::operator=(const Lizard& rhs) { if (this == &rhs) return *this;
Animal::operator=(rhs); ... }
class AbstractAnimal{ protected: AbstractAnimal& operator=(const AbstractAnimal& rhs);
public: virtual ~AbstractAnimal() = 0; ... };
class Animal: public AbstractAnimal{ public: Animal& operator=(const Animal& rhs); ... };
class Lizard: public AbstractAnimal{ public: Lizard& operator=(const Lizard& rhs); ... };
class Chicken: public AbstractAnimal{ public: Chicken& operator=(const Chicken& rhs); ... };
|
讨论总结
通过上面的例子,我们可以看到,当一个具体类作为基类,会导致派生类的设计产生出切片问题,异型赋值问题;而定义一个抽象基类就可以完美解决错误设计到来的问题,这也提醒了我们,基类的角色应该是一个不包含成员变量的抽象类,且它的存在就是一是为了支持派生类可以调用基类的operator=,二就是不会导致派生类对象之间异型赋值,切片赋值,确保一个派生类对象只能与相同类型的对象相互赋值。
如何设计继承体系
当你有两个具体类C1,C2,如果希望C2以public方式继承C1,合理的设计应该是单独创建一个抽象类A,然后让C1和C2都继承A; 而一开始C2继承C1的初衷在于他们之间有某些共同的东西,因此要采用上述合理设计就应该找出这些“共同的东西”是什么,然后将其形式化为抽象基类class A,并定义完好的member function和语义
返例:需要C2继承C1时,先定义C2的抽象类版本和的具体类版本,然后让具体类都继承抽象类。不要这样做的原因在于,这会导致太多的classes,提高维护难度;面向对象设计的目标是辨识出一些有用的抽象性,并强迫他们成为抽象类。这里的抽象性的一个判别方法就是在多个环境下都需要的性质。
何时需要定义抽象类
在设计一个类时,不应该马上就同时设计其抽象类,只有当所有概念明白后,且有必要让一个类继承另一个类时才定义抽象类进行继承;也就是说,当继承关系出现时,就意味着需要一个抽象类!
继承程序库中(无法修改)的具体类时,又该如何?
无法修改程序库以安插一个新的抽象类,所以选择有限:
1、将你的具体类继承程序库的具体类,但是要验证assignment问题(切片、异型)
2、找到程序库继承体系中更高层的抽象类,其中势必有你需要的大部分功能
3、实例化这个程序库具体类对象作为你的类成员,但这意味着程序库更新时你需要验证并更新使用它的类
4、在non-member functions中使用这些程序库具体类。功能虽然实现了,但是不好维护,效率也不够好。