确定你的public继承塑模出is-a关系——条款32

 

        如果你令class D(“Derived”)以public形式继承class B(“Base”),你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意识是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种B对象。反之如果你需要一个D对象,B对象无法效劳,虽然每个D对象都是一个B对象,反之并不成立。

       C++对于“public继承”严格奉行上述见解。考虑以下例子:

class Person { ... };
class Student:public Person { ... };

        根据生活经验我们知道,每个学生都是人,但并非每个人都是学生。这便是这个继承体系的主张。我们预期,对人可以成立的每一件事——例如每个人都有生日——对学生也都成立。但我们并不预期对学生科成立的每一件事——例如他或她就读于某所学校——对人也成立。人的概念比学生更一般化,学生是人的一种特殊形式。

        于是,承上所述,在C++领域中,任何函数如果期望获得一个类型为Person的实参,都也愿意接受一个Student对象:

void eat(const Person& p);      // 任何人都会吃
void study(const Student& s);   // 只有学生才到校学习
Person p;                       // p是人
Student s;                      // s是学生
eat(p);                         // 没问题,p是人
eat(s);                         // 没问题,s是学生,而学生也是(is-a)人
study(s);                       // 没问题,s是学生
study(p);                       // 错误!,p不是学生

        这个论点只对public继承才成立,只有当Student以public形式继承Person,C++的行为才会如我所描述。private继承的意义与此完全不同(见条款39),至于protected继承,那是一种其意义至今仍然困惑我的东西。

        public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你,例如,企鹅是一种鸟,这是事实。鸟可以飞,这也是事实。如果我们天真地以C++描述这层关系,结果如下:

class Bird {
public:
	virtual void fly();         //鸟可以飞
	...
};
class Penguin:public Bird {     // 企鹅是一种鸟
	...
};

        在这个例子中,我们成了不严谨语言下的牺牲品。当我们说鸟会飞的时候,我们真正的意思并不是说所有的鸟都会飞,我们要说的只是一般的鸟都有飞行能力。如果严谨一点,我们应该承认一个事实:有数种鸟不会飞。我们来到以下继承关系,它塑模出较佳的真实性:

class Bird {
	...                           // 没有声明fly函数
};
class FlyingBird:public Bird {
public:
	virtual void fly();         
	...
};
class Penguin:public Bird {     
	...                          // 没有声明fly函数
};

        这样的继承体系比原先的设计更能忠实反映我们真正的意思。即便如此,此刻我们仍然未能完全处理好这些鸟事,因为对某些软件系统而言,可能不需要区分会飞的鸟和不会飞的鸟。这反映出一个事实,世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来。

        在看一个例子,class Square应该以public形式继承Rectangle class吗?

                                                            确定你的public继承塑模出is-a关系——条款32_第1张图片

        你说,“当然应该如此!每个人都知道正方形是一种矩形,反之则不一定”,这是真理,至少学校是这么教的。但是我不认为我们还在象牙塔内。

        考虑这段代码:

class Rectangle {
public:
	virtual void setHeight(int newHeight);
	virtual void setWidth(int newWidth);
	virtual void height() const;
	virtual void width() const;
	...
};
void makeBigger(Rectangle& r)           // 这个函数用以增加r的面积
{
	int oldHeight = r.height();
	r.setWidth(r.width() + 10);         // 为r的宽度加10
	assert(r.height() == oldHeight);    // 判断r的高度是否未曾改变
}

        显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度;r的高度从未被更改。

        现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:

class Square:public Rectangle { ... };
Square s;
...
assert(s.width() == s.height());  // 这对所有正方形一定为真
makeBigger(s);                    // 由于继承,s是一种(is-a)矩形,所有我们可以增加面积
assert(s.width() == s.height());  // 对所有正方形应该仍然为真

        这也很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的高度和宽度相同。

        但现在我们遇上了一个问题。我们如何调解下面各个assert判断式:

  • 调用makeBigger之前,s的高度和宽度相同;
  • 在makeBigger函数内,s的宽度改变,但高度不变;
  • makeBigger返回之后,s的高度再度和其宽度相同。(主要s是以by references方式传给makeBigger,所以makeBigger修改的是s自身,不是s的副本。)

        怎么样?结果不是我们所期待的。因为最后第二个assert的结果不为真了~

        不要因为你发展经年的软件直觉与面向对象观念打交道的过程中失去效用,便心慌意乱起来。那些知识还是有价值的,但现在你已经为你的“设计”军械库加上继承这门大炮,你必须为你的直觉添加新的洞察力,以便引导你适当运用“继承”这一支神兵利器。

        is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-terms-of(根据某物实现出)。这些关系将在条款38和39讨论。将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以你应该确定你确实了解这些个“classes相互关系”之间的差异,并知道如何在C++中最好地塑造它们。

请记住

  • “public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

上述关于鸟儿和矩形的例子并不是说我们在设计的时候不能这样用继承关系,因为没有适用于所有软件的完美设计。我们设计项目时根据实际情况出发,同时要考虑到可能出现像鸟儿和矩形那种特殊情况,避免出现设计错误,最后能达到最佳设计效果。

你可能感兴趣的:(Effective,C++)