条款34 区分接口继承和实现继承

总结:

1.  接口继承&实现继承不同。在public继承之下,derived classes总是继承base class的接口。

2.  pure virtual纯虚函数只具体指定接口继承。

3.  Impure virtual普通虚函数具体指定接口继承及缺省实现继承。

4.  non-virtual普通函数具体指定接口继承以及强制性实现继承。

     公有继承的概念看似简单,似乎很轻易就浮出水面,然而仔细审度之后,我们会发现公有继承的概念实际上包含两个相互独立的部分:函数接口的继承和函数实现的继承。二者之间的差别恰与函数声明和函数实现之间相异之处等价。成员函数的接口总是被继承,因为public继承意味is-a(文章:条款32 确定你的public继承塑模出is-a关系)。

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 {...};


一、纯虚函数

    pure virtual 函数两个突出特征:(1)必须被任何“继承了它们”的具象class重新声明;(2)它们在抽象class中通常没有定义。

    声明一个pure virtual函数的目的是为了让derived class只继承函数接口(纯虚函数draw的声明是对其派生类设计者说:你必须提供draw函数,但是我不干涉你如何实现它)。这对draw函数的设计是合理的:任何Shape对象都应该是可以绘出来的,但是Shape类无法为该函数提供合理的缺省实现,毕竟椭圆的绘制法和矩形绘制法迥异。

    令人意外的是,竟然可以为纯虚函数提供定义,也就是为它提供一份实现代码,C++对它并没有任何怨言,但调用它的唯一途径是“调用时明确指出期class的名称”:

Shap<span style="font-family:宋体;">e *ps = new Shape;  <span style="line-height:28px; font-family:宋体"><span style="background-color:rgb(255,255,255)"></span></span></span>//错误,Shape是抽象类,不能<span style="font-family:宋体;">对</span>抽象类进行实例化<span style="font-family:宋体;"><span style="line-height:28px; font-family:宋体"><span style="background-color:rgb(255,255,255)"></span></span></span>Sharp* ps1 = new Rectangle;  
ps1->draw();
Sharp* ps2 = new Rectangle;
ps2->draw();
ps1->Shape::draw();     //调用Shape的draw
ps2->Shape::draw();
这样对纯虚函数的实现用途有限;但是一如稍后你将看到,它可以实现一种机制:为普通虚函数提供更平常更安全的缺省实现。

二、普通虚函数

例如上面Shape类的普通虚函数error();

声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现(普通虚函数是告诉派生类的设计者:你必须支持一个error函数,但如果你不想自己写一个,可以使用基类Shape提供的缺省版本)。

但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险:

class Airport {...};              //用以表示机场
class Airplane { 
public: 
    virtual void fly(const Airport& destination); 
};
void Airplane::fly(const Airport& destination) { 
    //缺省代码,将飞机飞至指定目的地 
} 
class ModelA : public Airplane {...};
class ModelB : public Airplane {...};
class ModelC : public Airplane {
...//未声明fly函数,但它并不希望缺省飞行
};
Airport PDX(...);        //PDX是我家附近的机场
Airplane* pa = new ModelC; 
... 
pa->fly(PDX);  //未说出“我要的情况下就继承了该缺省行为,酿成大灾难”

        A型和B型两种飞机,它们以相同方式飞行,因此有了上述的继承方式。基类Airplane::fly被声明为虚函数,为了避免A和B撰写相同的飞行代码,为Airplane::fly提供了缺省的飞行方式,这被A和B继承。但是新的C型飞机的飞行方式和A、B的飞行方式不一样,但是如上代码:忘记为C定义fly函数,这将是灾难。

解决方法1

        上述问题不在Airplane::fly有缺省行为,而在于C类在未明确说明要继承该缺省行为的情况下却继承了该行为。幸运的是可以轻易通过“只有派生类明确说明要用基类缺省行为才为派生类提供缺省实现,否则不为派生类提供缺省行为”。此间伎俩在于切断“虚函数接口”和“缺省实现”之间的连接

class Airplane { 
public: 
    virtual void fly(const Airport& destination) = 0;   //注意,fly被声明成了虚函数,仅提供飞行接口
    ... 
protected: 
    void defaultFly(const Airport& destination);    //这是fly的缺省行为,被用独立的函数呈现
}; 
void Airplane::defaultFly(const Airport& destination) { 
    //缺省行为,将飞机飞至目的地 
}
现在想要使用缺省实现(A类和B类),可以在其fly函数中对defaultFly做一个inline调用:

class ModelA: public Airplane { 
public: 
    virtual void fly(const Airport& destination) {  //实现缺省飞行行为
        defaultFly(destination); 
    } 
    ... 
}; 
class ModelB: public Airplane { 
public: 
    virtual void fly(const Airport& destination) { 
        defaultFly(destination); 
    } 
    ... 
};
      注意,defaultFly是Protected类型,因为它是Airplane以及其派生类的实现细节;乘客应该只关心飞机能不能飞,而不在意飞机怎么飞。Airplane::defaultFly是非虚函数,这很重要:因为没有任何一个派生类应该重新定义该函数。
    现在ModelC不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:

class ModelC: public Airplane { 
public: 
    virtual void fly(const Airport& destination); 
    ... 
}; 
void ModelC::fly(const Airport& destination) { 
    //将C型飞机飞至指定的目的地 
}

解决方法2

    有些人反对以不同的函数分别提供接口和缺省实现,向上述的fly和defaultFly那样。他们担心过度雷同的函数名称会引起类命名空间污染问题。但是他们也同意,接口和缺省实现应该分开。方法:利用“纯虚函数必须在派生类中重新声明,但是它们可以用于自己的实现”这一事实。下面是Airplane继承体系如何给pure virtual函数一份定义:

class Airplane { 
public: 
    virtual void fly(const Airport& destination) = 0; 
    ... 
}; 
void Airplane::fly(const Airport& destination)  {   //纯虚函数实现
    //缺省行为,将飞机飞至指定目的地 
} 
class ModelA: public Airplane { 
public: 
    virtual void fly(const Airport& destination) { 
        Airplane::fly(destination); 
    } 
}; 
class ModelB: public Airplane { 
public: 
    virtual void fly(const Airport& destination) { 
        Airplane::fly(destination); 
    } 
}; 
class ModelC: public Airplane { 
public: 
    virtual void fly(const Airport& destination) 
    ... 
}; 
void ModelC::fly(const Airport& destination) { 
    // 将C型飞机飞至指定目的地 
}
    这个实现和前一个类似,只是用纯虚函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在的fly被分割成两个基本要素:其声明部分的是接口(那个派生类必须使用的),其定义部分则表现出缺省行为(那是派生类可能使用的,但是只有在它们明确提出申请时才会)。

三、非虚函数

看Shape的非虚函数objectID:

class Shape { 
public: 
    ...
    int objectID() const; 
}; 
    如果成员函数是个non-virtual函数,意味着它并不打算在derived classes中有不同的行为。non-virtual 成员函数所表现的 不变性凌驾其特异性 ,无论derived class变得多么特异化,它的行为都不可以改变,这是条款36讨论的重点。
     声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现

    来看Shape::objectID的声明:可以想做是“每个Shape对象都有一个用来产生对象识别码的函数:此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何derived class都不应该尝试改变其行为”。


四、两个常见错误

1、将所有函数声明为非虚函数

       这使得派生类没有多余的空间进行特化工作。非虚析构函数会带来问题(参考文章条款07 为多态基类声明virtual析构函数)。当然,设计一个并不想成为基类的类就不要有虚函数了。但将所有函数声明为非虚函数是因为担心虚函数带来的效率成本,那么这种声明就是错误的。实际上,任何类如果打算作为虚基类,就会拥有若干个虚函数(条款07 为多态基类声明virtual析构函数)。

       如果你关心虚函数成本,请注意80-20法则:一个典型的程序80%的执行时间会花费在20%的代码身上。这一法则很重要,因为它意味着,平均而言你的函数调用中可以有80%是虚函数而不会冲击程序的大体效率。所以当担心virtual函数的成本前,先将心力放在那举足轻重的20%代码上,它才是关键。

2、将所有函数声明为虚函数

有时候这样是正确的,例如条款31的interface classes。然而这也可能是类设计者缺乏坚定立场的前兆。

某些函数就是不该在派生类中重新定义的,那么就应该将其设计为非虚函数,毕竟虚函数有额外成本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值