Effective C++ 第六章——继承与面向对象设计

介绍了条款32~33

Posted on May 16, 2017 in Effective_Cpp, ReadBook


条款32:确定你的public继承塑模出is-a关系
条款33:避免遮掩继承而来的名称
条款34:区分接口继承与实现继承
条款35:考虑virtual函数以外的其他选择
    通过Non-Virtual Interface手法实现Template Method模式
    Function Pointers实现Strategy模式
    通过tr1::function完成Strategy模式
    古典的Strategy模式
条款36:绝不重新定义继承而来的非虚函数
条款37:绝不重新定义继承而来的缺省参数值
条款38:通过复合塑模出“has-a”或“is-implemented-in-terms-of”
条款39:明智而审慎地使用private继承
条款40:明智而审慎地使用多重继承


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

如果令class D以public形式继承class B,那么便是告诉C++编译器说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。 
即主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,反之则不成立。

以后在继承的时候需要特别注意,处理一些特殊的关系,如P151class Bird是否应该加入fly()函数,因为有些鸟是不会飞的,如class Penguin,可以解决的方法有两个:一是从编译器发现Penguin::fly()的错误P152,二是在运行期发出错误提示P152

又如P153:正方形类与矩阵类的区别。一些适合矩阵类的普通函数是否适合正方形类,若不适合就违反is-a了。

is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)与is-implemented-in-terms-of(根据某物实现出)。

条款33:避免遮掩继承而来的名称

C++的名称遮掩规则常见与一个局部内的变量x,遮掩了全局同名变量x。在类的继承中也存在同样情况。本条款只谈名称,与其他无关,类内的enums,nested classes和typedefs同样适合。

当在派生类中调用一个名称的时候(没在之前加任何限定符),查找作用域看看有没有该声明式的顺序是:

1.local作用域(函数覆盖的作用域)

2.外围作用域(派生类覆盖的作用域)

3.基类覆盖的作用域

4.namespaces的作用域

5.global作用域

如下面的例子中

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();
    ......
}

以作用域为基础的“名称遮掩规则”并没有改变(注意不是以函数签名遮掩),因此基类内所有名为mf1与mf3的函数都被派生类的mf1与mf3函数遮掩掉。 
所以:

Derived d;
int x;
......
d.mf1();    //yes,调用派生类的函数
d.mf1(x);    //no
d.mf2();    //yes,调用基类的函数
d.mf3();    //yes,调用派生类的函数
d.mf3(x);    //no

因此名称遮掩规则是不管派生类和基类内的函数的签名式的,而且也不论是否虚函数。 
这么做是为了防止在程序库或者应用框架内建立新的派生类的时候附带地从疏远的基类继承重载函数。

当你希望继承那些重载函数的时候,可以使用using声明式:

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:    using Base::mf1;    using Base::mf3;    virtual void mf1();    void mf3();    void mf4();    ......}

现在,上面的例子都可以运行了。

这个使用using声明式继承基类的所有函数,在public中是必须得存在的,因为“is-a”关系,将基类public性质的函数放在派生类的public下;在private继承中却是不能这么做(将基类public性质的函数放在派生类的public下),若真的想这样做就必需使用转交函数

class Base{private:    int x;public:    virtual void mf1() = 0;       virtual void mf1(int);    ......};class Derived:private Base{public:    virtual void mf1(){        Base::mf1();    }    ......}

转交函数也使不支持using声明式的旧编译器提供另一条路。

条款34:区分接口继承与实现继承

public继承概念由两部分组成:函数接口继承函数实现继承 
分为4种:


应用情况解决方案

只希望派生类只继承成员函数的接口(也就是声明)纯虚函数

派生类同时继承函数的接口和实现虚函数

派生类同时继承函数的接口和实现而且可以overwrite函数方法一:“virtual函数接口”和其“缺省实现”分离。
方法二:在纯虚函数中写上缺省实现


派生类同时继承函数的接口和实现而且可同时禁止overwrite函数非虚函数

- 成员函数的接口总是会被继承。


1.声明一个纯虚函数的目的是为了让派生类只继承函数的接口。

2.声明简朴的虚函数的目的,是让派生类继承该函数的接口和缺省实现。

3.声明非虚函数的目的是为了令派生类继承函数的接口以及一份强制性的实现。非虚函数代表的意义是不变性凌驾特异性。

条款35:考虑virtual函数以外的其他选择

回顾上面,虚函数的作用是提供接口与可重写的实现,但有些时候可以加以改进:

通过Non-Virtual Interface手法实现Template Method模式

这个流派主张virtual函数应该几乎总是private函数,例如下面的实现

class GameCharacter{
public:
    int healthValue() const    //派生类不能重新定义它
    {    
        ...    //做一些事前工作
        int retVal = doHealthValue();
        ...    //做一些事后工作
        return retVal ;
    }
    ...
private:
    virtual  int doHealthValue() const    //派生类可以重新定义它
    {
        ...    //缺省的算法
    }
    ...
}

这就是NVI手法:客户通过public non-virtual成员函数间接调用private virtual函数。 
NVI允许用户赋予函数新功能,但是何时被调用则是基类说了算。

通过Function Pointers实现Strategy模式

先看看实现代码:

class GameCharacter;    //前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    typedef int (*HealthValueCalcFunc)(const GameCharacter&);
    explicit GameCharacter(HealthValueCalcFunc func = defaultHealthCalc) :
        _healthFunc(func)
    {}
    int healthValue() const    //派生类不能重新定义它
    {    
        return _healthFunc(*this);
    }
    ...
private:
   HealthValueCalcFunc _healthFunc;
    ...
}

这样实现提供更大的弹性:

1.同一个类的不同实体可以有不同的计算函数

2.实体的计算函数是可以在运行期更换的 
缺点是降低了类的封装性。

通过tr1::function完成Strategy模式

实现方法与上面差不多,只是这样可以提供更大的灵活性,一个普通函数,类的成员函数与函数对象(P174有举例)都可以通过tr1::function传递。

class GameCharacter;    //前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    typedef std::tr1::function<int (const GameCharacter&)> HealthValueCalcFunc ;
    explicit GameCharacter(HealthValueCalcFunc func = defaultHealthCalc) :
        _healthFunc(func)
    {}
    int healthValue() const    //派生类不能重新定义它
    {    
        return _healthFunc(*this);
    }
    ...
private:
   HealthValueCalcFunc _healthFunc;
    ...
}

古典的Strategy模式

通过聚合,将类HealCalcFunc(抽象类)的指针(或智能指针)成为类GameCharacter的一个成员。通过将HealCalcFunc的一个派生类指针传入去GameCharacter类的实例中,并通过它的某个函数来计算需求。

条款36:绝不重新定义继承而来的非虚函数

因为非虚函数是类的静态类型,是通过静态绑定的,overwrite非虚函数会带来很多麻烦,同时违反了“is-a”规则。

条款37:绝不重新定义继承而来的缺省参数值

原因是上一条的类似,这里讨论的缺省参数值虚函数的缺省参数值,因为非虚函数是禁止overwrite。 
虚函数是动态绑定而其缺省参数是静态绑定,当继承的时候,在派生类改写缺省参数是一个bad idea;而不写缺省参数值也是有问题的:

1.当用类的值来调用该函数的时候一定要填入参数值(不能缺省了)

2.当用指针或者引用调用次函数的时候,才可以不指定值

那么唯一能做的是派生类与基类都得在这个虚函数的缺省函数值填写一模一样吗?这样代码既重复了,又有连带关系,当基类的缺省函数值改了之后,派生类的也要跟着改了。 
聪明的做法是不用virtual:如条款35的NVI。

条款38:通过复合塑模出“has-a”或“is-implemented-in-terms-of”

复合是类型之间的一种关系,当某种类型的对象内含有其它种类型的对象。

has-a关系比较容易与is-a关系区分与辨认。 
而is-a与is-implemented-in-terms-of则是两个类之间并非is-a关系,但是新的类可以根据一个旧的类对象来实现,如set与listP185

在应用域,复合意味has-a。在实现域,复合意味着“is-implemented-in-terms-of”

条款39:明智而审慎地使用private继承

private继承的规则:

1.派生类与基类不是is-a的关系,编译器不会自动将一个派生类转换为一个基类。

2.从private基类继承而来的成员在派生类中都会变成private属性。

private继承意味着is-implemented-in-terms-of(根据某物实现出)。如果你让派生类以private的方式继承基类,那么用意是为了采用基类内已经实现的某些特性,而非因为基类与派生类存在有任何观念上的关系。 
private继承只是一种实现技术,基类的特性成为私有的实现细节,派生类根据基类对象实现而得。

复合与private同样是is-implemented-in-terms-of,尽可能使用复合,必要的时候才使用私有继承:

1.当基类的protect成员(需要调用它)和/或virtual函数被牵扯进来(需要改写它)的时候。其实复合也可以实现P189,好处有两个:一是限制派生类的某些行为,而是减低编译依存性。

2.内存空间需要十分有限的时候:空白基类最优化EBO。属性空白基类中emptyP191

当面对“并不存在is-a关系”的两个classes,其中一个需要访问另一个的protected成员或者需要重新定义其一或多个virtual函数的时候,private继承极有可能成为正统设计策略。而EBO很少成为private继承的正当理由。

条款40:明智而审慎地使用多重继承

多次继承的时候可能出现调用函数的歧义性,此时需要明确一下是调用哪一个基类的同名函数。

在菱形继承中可能会重复地继承了同一个含义的变量(编译器默认是这样的),此时可以考虑virtual继承,使用虚继承的那些派生类所产生的对象往往

1.比不使用的派生类所产生的体积大;

2.访问基类成员变量的时候也比较慢;

3.同时支配“virtual base classes初始化”的规则比起non-virtual bases的情况远为复杂且不乐观。

所以忠告是:

1.非必要不使用virtual bases。

2.若必须要使用,则避免在virtual base classes上放置数据。

P195展示了多重继承的用法,如需要public继承某个interface class,和private继承某个协助实现的class的两相结合。