Effective C++阅读笔记(四):继承和面向对象

来源:互联网 发布:手机涂鸦软件 编辑:程序博客网 时间:2024/06/14 03:51

四、继承和面向对象设计

C++提供了多种很令人困惑的面向对象构造部件,包括公有、保护和私有基类;虚拟和非虚拟基类;虚拟和非虚拟成员函数。这些部件不仅互相之间有联系,还和C++的其它部分相互作用。所以,对于每种部件的含义、什么时候该用它们、怎样最好地和C++中非面向对象部分相结合 ----要想真正理解这些,就要付出艰苦的努力。


条款23: 使公有继承体现 "是一个" 的含义

class B { ... };
class D: public B { ... };

我们可以这样理解:

类型D 的每一个对象也是类型B 的一个对象,但反之不成立;

B 表示一个比D 更广泛的概念,D 表示一个比B 更特定概念;

任何可以使用类型B 的对象的地方,类型D 的对象也可以使用,反之则不行。

看这样一个设计:正方形可以从长方形公有继承吗?至少根据我们的常识,正方形是一个长方形,但反正不成立。

class Rectangle {public:    virtual void setHeight(int newHeight);    virtual void setWidth(int newWidth);    virtual int height() const; // 返回当前值    virtual int width() const; // 返回当前值    ...private:    int height;    int width;};void makeBigger(Rectangle& r) // 增加r 面积的函数{    int oldHeight = r.height();    r.setWidth(r.width() + 10); // 对r 的宽度增加10    assert(r.height() == oldHeight); // 断言r 的高度未变}

在调用以长方形类Rectangle作参数 的makeBigger函数时,增加宽度之后,该类仍为Rectangle类。但是用正方形类作参数时,宽度增加之后,参数已经不满足正方形类的性质了(宽和高不相等)。公有继承的性质有:任何可以使用基类 的对象的地方,派生类的对象也可以使用。因此用公有继承来表示矩形和正方形的关系只会是错误,无法保证程序正常工作。


条款24: 区分接口继承和实现继承

公有继承由两个可分的部分组成:函数接口的继承和函数实现的继承。

继承的选择:

  • 派生类只继承成员函数的接口(声明)---- 纯虚函数
  • 派生类同时继承函数的接口和实现,但允许派生类改写实现 ---- 虚函数;
  • 派生类同时继承接口和实现,并且不允许派生类改写任何----非虚函数。

利用几何形状类来理解继承的这些选择:

class Shape {public:    virtual void draw() const = 0; //纯虚函数,抽象类    virtual void error(const string& msg); //虚函数    int objectID() const;    ...};class Rectangle: public Shape { ... };class Ellipse: public Shape { ... };

特征:

非虚函数,表明了一种特殊性上的不变性,使派生类继承函数的接口和强制性实现。

纯虚函数,它们必须在继承了它们的任何具体类中重新声明,而且它们在抽象类中往往没有定义。

普通虚函数,派生类继承了函数的接口,但简单虚函数一般还提供了实现,派生类可以选择改写它们或不改写它们。

请注意:

为简单虚函数同时提供函数声明和缺省实现是很危险的,因为子类可以不用明确地声明就可以继承这一行为,因此要切断虚函数的接口和它的缺省实现之间的联系。可以借助于这一事实:纯虚函数必须在子类中重新声明,但它还是可以在基类中有自己的实现。

class Shape {public:     virtual void draw() const = 0; //纯虚函数,抽象类     virtual void error(const string& msg) = 0; //改写成纯虚函数,使接口和缺省分离     int objectID() const;     ...};void Shape::error( cosnt string &msg ){ ... //缺省error实现}class Rectangle: public Shape {public:    virtual void draw() const;    virtual void error( const string &msg )    {        Shape::error(msg); //子类需要继承缺省时,需要主动请求调用    }    ...};class Ellipse: public Shape {public:     virtual void draw() const;     virtual void error( const string &msg )     {          ... //子类不继承缺省时,需要自定义实现     }     ...};

经常犯的两个错误:

  1. 第一个是把所有的函数都声明为非虚函数。这就使得派生类没有特殊化的余地;非虚析构函数尤其会出问题。当然,设计出来的类不准备作为基类使用也是完全合理的。
  2. 另一个常见的问题是将所有的函数都声明为虚函数。有时这没错 ---- 比如,协议类(Protocol class)。但是这样做往往表现了类的设计者缺乏表明坚定立场的勇气。一些函数不能在派生类中重定义,只要是这种情况,就要旗帜鲜明地将它声明为非虚函数,不能让你的函数好象可以为任何人做任何事 。


条款25: 决不要重新定义继承而来的非虚函数

class B {public:    void mf();    ...};class D: public B { public:    void mf(); // 隐藏了B::mf;     ...};
如果进行下面的调用:
D x;B *pb = &x;D *pd = &x;pB->mf(); // 调用B::mfpD->mf(); // 调用 D::mf,行为不同

原因在于:非虚函数是静态绑定的,相反,虚函数是动态绑定的。

这意味着,因为pb 被声明为指向B 的指针类型,通过pb 调用非虚函数时将总是调用那些定义在类B 中的函数 ---- 即使pb指向的是派生类D的对象。果mf 是虚函数,通过pb 或pd 调用mf 时都将导致调用D::mf,因为pb 和pd 实际上指向的都是类型D 的对象。


条款26: 决不要重新定义继承而来的缺省参数值

缺省参数只能作为函数的一部分而存在,重定义缺省参数值的唯一方法是重定义继承的一个有缺省参数值的虚函数。

原因在于:虚函数是动态绑定而缺省参数值是静态绑定的

这意味着你最终可能调用的是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。如果缺省参数值被动态绑定,编译器就必须想办法为虚函数在运行时确定合适的缺省值,这将比现在采用的在编译阶段确定缺省值的机制更慢更复杂。


条款27: 区分继承和模板

在进行类的设计时,涉及到"类的行为" 和 "类所操作的对象的类型"之间的关系。要处理的都是各种不同的类型,你可以根据下面的原则进行选择:

  • 如果对象类型不影响类中函数的行为,就要使用模板来生成类;
  • 如果对象类型影响类中函数的行为,既要使用虚函数,从而要使用继承。


条款28: 明智地使用私有继承

在公有继承中,为了使某个函数成功调用,编译器可以在必要时隐式地将派生类转换成基类。

私有继承特征:

  • 和公有继承相反,编译器一般不会将派生类对象转换成基类对象;
  • 从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员。

私有继承意味着只是继承实现,接口会被忽略。如果D 私有继承于B,就是说D 对象在实现中用到了B 对象。私有继承与包含(分层)都是意味着“用...来实现”,它们之间的差异:

  • 私有继承的情况下可以在派生类中设置成员函数来访问基类保护成员,但是包含却不能;
  • 私有继承使派生类可以重新定义基类的虚函数,包含则没有这个特性;
  • 包含使程序清晰易懂,私有继承使类之间的关系变得抽象;
  • 包含可能会使文件之间编译的依赖性增加。

选择方法:

  • 尽可能地使用分层,必须时才使用私有继承;
  • 在需要访问保护成员或者重载虚函数的情况下,使用私有继承。
1 0
原创粉丝点击