欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Effective C++条款32:继承与面向对象之(确定你的public继承塑模出is-a关系)

程序员文章站 2022-07-15 12:38:43
...

一、“is-a”的概念

  • 以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-1”(是一种)的关系
  • 如果你令class D以public形式继承class B,你便是告诉编译器:
    • 每一个类型为D的对象同时也是一个类型为B的对象。反之不是
    • B对象可使用的地方,D对象一样可以使用。反之不是

演示案例

  • 下面Student类public继承于Person
class Person {};
class Student :public Person {};
  • 任何获得类型为Person(pointer-to-Person或reference-to-Person)的实参,都可以接受一个Student(pointer-to-Student或reference-to-Student)对象
  • 例如:
void eat(const Person& p);
void study(const Student& s);

int main()
{
    Person p;
    Student s;

    eat(p);    //正确
    eat(s);    //正确
    study(s);  //正确
    study(p);  //错误
 
    return 0;
}
  • 上面的规则只对public继承才成立。private、protected不成立

二、设计正确的继承模型

  • 鸟可以飞,企鹅也是一种鸟。于是我们可能设计下面错误的继承模型:
    • 企鹅虽然属于鸟类,但是企鹅不会飞
    • 设计中,我们错误的将鸟类中的fly()虚函数派生给了Penguin类
//鸟类
class Bird {
public:
    virtual void fly();
};

//企鹅,也继承了fly()虚函数
class Penguin :public Bird {};
  • 我们应该修改上面的代码,下面才是合适的模型:
//鸟类
class Bird {
    //无fly()函数
};

//会飞的鸟类
class FlyingBird :public Bird {
public:
    virtual void fly();
};

//企鹅不会飞
class Penguin :public Bird {

};

三、以“编译期”确认关系代替“运行期”确认关系

  • 紧接着上面鸟类与企鹅的问题
  • 企鹅不会飞,但是我们仍然让Bird定义fly()函数,然后让Penguin继承于Bird,与上面不同的是,我们让Penguin在执行fly()函数的时候报出一个错误(运行期执行)代码如下:
class Bird {
public:
    virtual void fly();
};

void error(const std::string& msg);
class Penguin :public Bird {
public:
    virtual void fly() {
        error("Attempt to make a penguin fly!");
    }
};
  • 上面的代码是在运行期检查这种错误的
  • 下面我们设计让编译器在编译的时候检查出企鹅不会飞这种错误。代码如下:
class Bird {
    //无fly()函数
};


class Penguin :public Bird {
    //...
};

Penguin p;
p.fly();
  • 总结:
    • 上面我们介绍了检测“企鹅不会飞”这种错误的两种方式。一种为在运行期检测,一种为在编译期检测
    • 当然,我们希望在编译期的时候就确定企鹅不会飞这种关系,因此更希望以第二种方式(编译期)代替第一种方式(运行期)来设计继承关系

四、is-a模型的一些例外

考虑这样一个演示案例

  • 我们让正方形类(Square)public继承于矩形类(Rectangle)。如下所示:

Effective C++条款32:继承与面向对象之(确定你的public继承塑模出is-a关系)

  • 矩形类的代码定义如下:
class Rectangle {
public:
    virtual void setHeight(int newHeight); //设置高
    virtual void setWidth(int newWidth);   //设置宽

    virtual void height()const; //返回高
    virtual void width()const;  //返回宽
};
  • 现在有下面这个函数,下面函数中的assert永远为真,因为函数只改变了宽度,而没有改变高度:
//这个函数用来增加r的面积
void makeBigger(Rectangle& r)
{
    int oldHeight = r.height(); //取得旧高度
    
    r.setWidth(r.width() + 10); //设置新宽度
    
    assert(r.height() == oldHeight); //判断高度是否改变
}
  • 正方形类的代码定义如下:
class Square :public Rectangle {
    //...
};
  • 现在有下面的代码:
Square s; //正方形类

//...

assert(s.width() == s.height()); //永远为真,因为正方形的宽和高相同
makeBigger(s); //由于继承,我们可以增加正方形的面积

//...
assert(s.width() == s.height()); //对所有正方形来说,应该还是为真
  • 现在考虑上面的代码:
    • 第一步,我们判断正方形的宽和高,根据原理,assert应该返回真
    • 第二步,调用makeBigger()函数,这个函数改变了宽度,而没有改变高度
    • 第三步,再次调用assert应该还是返回真,因为此处的s为正方形
  • 现在我们可以看到:
    • 前面我们虽然提到过,作用于基类的代码,使用派生类也可以执行
    • 但是此处我们可以看到,某些施行于矩形类中的代码(例如只改变宽度而不改变高度),在长方形中却不可以实施(因为长方形的宽度和高度应该保持一致)
    • is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-terms-of(根据某物实现出)。这些关系将在条款38和39介绍。在这些相互关系的塑造为is-a会造成错误设计

五、总结

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