昨天颓废了 今天*2
条款32:确定你的public继承塑模出is-a关系
Make sure public inheritance models “is-a”
使用C++语言进行面向对象编程时最重要的规则就是:publicinheritance意味着"is-a"关系。
如果令class D ("Derived") 从 class B ("Base") public继承,就是在告诉C++编译器(以及你的代码读者)每一个类型为D的对象也是一个类型为B的对象,但是反之则不然。B描绘了一个比D更一般的概念,D描述了一个比B更特殊的概念。在类型为B的对象可以使用的任何地方,类型为D的对象一样可以使用;在类型为D的对象可以使用的地方,类型为B的对象无法效劳。
考虑学生(Student)与人(Person)的例子,我们知道每一个学生都是一个人,但并不是每一个人都是一个学生。我们期望每一件对于人来说成立的事情(如出生日),对于一个学生来说也成立。我们不期望每一件对于学生来说成立的事情(如学校注册),对于普通人来说也成立。人的概念比学生的概念更普通,学生是人的一种特殊形式。
于是在C++中,任何函数如果期望获得一个类型为Person(或pointer-to-Person、reference-to-Person)的实参,都可以接受一个Student对象(或pointer-to-Student、reference-to-Student):
voideat(const Person& p); // anyone can eat
void study(const Student& s); // only students study
Personp;
Student s;
eat(p);// fine, p is a Person
eat(s); // fine, s is a Student, and a Student is-a Person
study(s);// fine
study(p); // error! p isn't a Student
这一点只对publicinheritance才成立。只有Student以public方式从Person派生,C++ 才有如上所描述的行为。private inheritance意味着完全不同的事情。
public inheritance和is-a等价听起来简单,但有时直觉会误导你。例如,企鹅是一种鸟没有问题,而鸟能飞也没有问题:
class Bird {
public:
virtual void fly(); // birds can fly
...
};
class Penguin:public Bird { // penguins arebirds
...
};
这个继承体系表示企鹅能飞,我们知道这不是真的。当我们说鸟能飞的时候,我们的意思并非是说所有种类的鸟都能飞,我们应该承认有几种不能飞的鸟,并提出如下继承体系:
class Bird {... };// 没有声明fly函数
class FlyingBird: public Bird {
public:
virtual void fly();
...
};
class Penguin: public Bird {... };// 没有声明fly函数
这个继承体系比最初的设计更忠实于反映我们真正的意思。其实世界上并不存在一个“适用于所有软件”的完美设计。最好的设计依赖于系统究竟期望做什么,无论现在还是未来。如果你的程序对飞行一无所知,那么不分辨能飞与不能飞的鸟可能就是一个非常完美的设计决策,因为你试图模拟的世界中就没有这样一种区分。
另一种思想观念处理企鹅和鸟的飞行问题,就是为企鹅重定义 fly 函数,让它产生一个运行时错误:
void error(const std::string& msg); // 定义于另外某处
class Penguin: public Bird {
public:
virtual void fly() { error("Attempt to make a penguin fly!");}
...
};
很重要的是,你必须认识到这里所说的某些东西和你所想可能不同。这不是说“企鹅不能飞”(可由编译器强制实施),而是说“企鹅能飞,但对它试图真的这样做就是一个错误”(运行期间测出来)。为了表达“企鹅不能飞”这个限制,确保不要为Penguin对象定义这样的函数:
classBird {... };
class Penguin: public Bird {... };
//均没有声明fly函数
如果你现在试图让企鹅飞,编译器将会惩罚你:
Penguinp;
p.fly(); // error!
好的接口可以在编译时防止非法代码,所以你应该用编译器阻止企鹅飞翔企图的设计代替只在运行时检测的设计。
class Square(正方形)应该从 class Rectangle(矩形)public继承吗?我们都知道一个正方形是一个矩形,但是反过来就不一定了。但考虑如下代码:
class Rectangle {
public:
virtual void setHeight(intnewHeight);
virtual void setWidth(int newWidth);
virtual int height() const; // 返回当前值
virtual int width() const;
...
};
void makeBigger(Rectangle& r) // 这个函数用以增加r的面积
{
int oldHeight = r.height();
r.setWidth(r.width() + 10); // r的宽度加10
assert(r.height() == oldHeight); // 判断r的高度是否未曾改变
}
显然断言应该永远不会失败。makeBigger仅仅改变了r的宽度,它的高度始终没有变化。现在考虑以下代码,使用public inheritance使得square可以像rectangle一样进行处理:
classSquare: public Rectangle {...};
Square s;
assert(s.width() == s.height()); // 这对所有正方形一定为真
makeBigger(s); // 由于继承,s也是矩形,所以我们可以增加其面积
assert(s.width() == s.height()); // 对所有正方形应该仍然为真
第二个断言也应该永远不会失败,因为正方形的宽度和高度是相等的。但是调用makeBigger之前,s的高度和它的宽度相等;在makeBigger内s的宽度发生变化,但是它的高度没有变化;从makeBigger返回之后,s的高度还要和它的宽度相等(注意s是通过by reference 方式传入makeBigger的,所以makeBigger改变的是s自身)。
在这种情况下,难点在于一些适用于矩形(宽度可以独立于高度而变化)的事情不适用于正方形(宽度和高度必须相等)。但是publicinheritance主张,适用于基类对象的每件事也适用于派生类对象。在矩形和正方形的例子中,这个主张失效,所以用public inheritance模拟它们的关系是完全错误的。编译器允许你这样做,但是它不能保证代码的行为正确。每一个程序员都必须认识到,通过编译的代码并不意味着它可以工作。
is-a关系并不是能存在于两个classes之间的唯一关系。另外两个常见的关系是"has-a"(有一个)和 "is-implemented-in-terms-of"(根据某物实现出)。用这些其它重要关系来不正确地模拟is-a而造成的C++设计错误并不罕见,所以你应该确保理解这些关系之间的不同,并知道在C++中如何才能用它们做最好的模拟。
· public inheritance 意味着 "is-a"。适用于 baseclasses 的每一件事也适用于 derived classes,因为每一个 derived class object 都是一个 baseclass object。
条款33:避免遮掩继承而来的名称
Avoid hiding inherited nams
这节与作用域(scope)有关:
intx; // global
void someFunc()
{
double x; // local
std::cin >> x; // read a new value for local x
}
这个读取数据的语句指涉local变量x,而不是global变量x,因为内层作用域的名字遮蔽外层作用域的名字。当编译器在someFunc的作用域中遇到名字 x 时,先巡视local作用域看看是否有什么东西叫这个名字。如果有,它们就不再检查其它作用域。在此例中,someFunc的x 类型为double,而globalx类型为int,但这不要紧。C++的名字遮掩规则仅仅是覆盖那个名字,而相对应的名字类型是否相同无关紧要。在此例中,double x覆盖了一个intx。
现在加入继承,实际中的运作方法是,派生类的作用域被嵌套在基类作用域之中。例如:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf4();
...
};
本例中包含的既有public也有private名字,既有成员变量也有成员函数。成员函数包括pure virtual、impurevirtual和non-virtua。例子中还可以包括其它类型的名字,例如enums,nestedclasses和 typedefs。在这里的讨论中唯一重要的事情是这些东西的名称,与它们是什么东西的名称毫不相关。这个示例中使用了singleinheritance,multiple inheritance下的行为可以从这里预见到。
假设mf4在derivedclass中部分实现代码如下:
void Derived::mf4()
{ mf2(); ... }
当编译器看到这里对名称 mf2 的使用,它就必须断定它指涉什么。它通过查找各作用域,看看有没有某个名为mf2的声明式。顺序如下:local作用域(也就是mf4的作用域)——它的包含作用域(也就是 class Derived 的作用域)——它的上一层包含作用域(也就是 base class 的作用域)——包含Base的namespace(如果有的话——global作用域。本例中它在base class 的作用域找到了名为mf2 的东西,所以搜索在那里停止。
再次考虑前面的示例,这一次我们重载mf1和mf3,并且为Derived增加一个mf3的版本:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};
以上代码导致的行为会使我们吃惊。基于作用域的名字覆盖规则依然生效,所以 baseclass 中所有名为 mf1 和 mf3 的函数被derived class 中的名为 mf1 和 mf3 的函数覆盖。Base::mf1 和 Base::mf3 不再被Derived 继承:
Derivedd;
int x;
d.mf1(); // fine, calls Derived::mf1
d.mf1(x); // error! Derived::mf1 hides Base::mf1
d.mf2(); // fine, calls Base::mf2
d.mf3(); // fine, calls Derived::mf3
d.mf3(x); // error! Derived::mf3 hides Base::mf3
就像你看到的,即使 base 和derived classes 中的函数具有不同的参数类型,不管函数是 virtual还是non-virtual,它也同样适用。这里Derived中(较小作用域)的函数mf 覆盖了具有不同类型的名为mf3的一个Base(较大作用域)函数。你可以用using声明式让baseclass中所有名为mf1和mf3的函数在Derived作用域里都可见(并且public),即在Derived里加入:
using Base::mf1;
using Base::mf3;
这意味着如果你从一个带有重载函数的base class继承,而且你只想重定义或替换它们中的一部分,你需要为每一个你不想覆盖的名字使用using声明式,否则你希望继承下来的名字会被覆盖。
在public inheritance 中,你将从你的base class继承所有的函数,否则就违反了public inheritance在基类和派生类之间的is-a关系。然而,private inheritance别有意义。例如,假设 Derived 从Base私有继承,而且Derived只想继承没有参数的那个mf1版本,使用using声明式会使得所有具有给定名称的函数在 derived class 中可见。你可以使用了一个简单的forwarding function(转交函数):
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
... // as before
};
class Derived: private Base {
public:
virtual void mf1() { Base::mf1(); } // 转交函数,成为默认的inline
...
};
...
Derivedd;
int x;
d.mf1(); // fine, calls Derived::mf1
d.mf1(x); // error! Base::mf1() is hidden
Inline转交函数的另一个作用是用于老式编译器,它们不支持用using声明式将“通过继承得到的名称”引入到derived class作用域。
· derived class 中的名字覆盖 base class 中的名字,在 public inheritance 中,这从来不是想要的。
· 为了使隐藏的名字重新可见,使用 using 声明式或者 forwarding functions(转交函数)。