六、继承与面向对象设计--条款38-40

条款38:根据复合塑模出has-a或者“根据某物实现出”

一、复合的概念

复合,是类型中的一种关系,指个某种类型的对象内含它种类型的对象的成员变量。 复合还称为分层(layering),内含(containment),聚合(aggregation)和内嵌(embedding).

二、区别is-a和has-a、is-implemented-in-terms-of

“public继承”带有is-a(是一种)的意义。而复合表示的是has-a,即有一种的意义。或者是is-implemented-in-terms-of,即根据某物实现出的意思。

class Address{...};
class PhoneNumber{...};
class Person
{
public:
    ...
private:
    string name;
    Address addr;
    PhoneNumber number;
    ...
};

这段代码中,有public继承,也有复合类型的变量。我们可以很容易区别is-a和has-a的关系。上述的addr和number都是Person中的一员。即有一个地址,有一个号码,而不是说,Person是一个地址,是一个号码。

而is-implemented-in-terms-of的话,是根据某物实现出来。现在我要实现一个新的set容器,我们想到的就是复用。复用别的类型去实现set,比如list。那么我们的类中就会有一个List的容器类。

也就是说,我们使用list来实现set,list是作为一个复合类型的成员变量。这就是is-implement-in-terms-of.

作者总结

复合(composition)的意义和public继承完全不同。

在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

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

一、无法自动转成Base类

如果我们以private形式继承的话,那么就无法将一个Derived类传递给一个接受Base类指针或引用为参数的类。

class Person{...};
class Student : public Person{...};
void eat(Person &p);
void study(Student &s);

Person p;
Student s;
eat(p); // 正确
study(p); // 错误

所以private继承并不意味着is-a关系。Derived classes不是一种Base class。

二、private继承

比如我们有一个定时器类:

class Timer
{
public:
    explicit Timer(int tickFrequence);
    virtual void onTick() const;
    ...
};

private继承只是为了使用Base类的功能而已,这两个类之间可以没有任何关系。比如一个窗口类要定时刷新,那么它可以继承一个定时器类(使用private继承),那它就拥有定时器类的功能。但是窗口类和定时器类可以说没有什么必要的联系。

如下:

class Widget : public Timer
{
private:
    virtual void onTick() const;
};

使用了private,就可以防止客户端的调用了。

三、private继承的替代方式

复合

对,就是上一篇文章中提到的复合。

我们使用private继承,是为了有特定的功能,那么我们何不如就直接将该类作为自己的一个成员呢?

class Widget
{
private:
    Timer timer;
    ...
};

嵌套类

首先,你或许会设计一个Widget使它可以被继承,又同时不想它重新定义一个onTick函数。如果我们使用Widget是继承Timer的,就不能实现我们的想法。所以采用嵌套类的形式,增加一个private成员变量:

class Widget
{
private:
    class WidgetTimer : public Timer
    {
    public:
        virtual onTick() const;
        ...
    };
    ...
    WidgetTimer WTimer;
};

这样对于直接使用private继承Timer类来说,还有一个优点:降低编译依存性

如果是直接使用private继承,那我们有出现Widget就有引用Timer的对应头文件。但是如果我们改成嵌套类了,如果WidgetTimer被移出Widget,那也只是需要在Widget有一个指针指向WidgetTimer类,然后带着一个简单的WidgetTimer的声明式即可,就不用#include 任何和Timer相关的东西。

四、EBO情形还是采用private继承

EBO,即空白基类最优化(Empty Base optimization).

我们都知道空白类,大多数编译器都会给它一个char的大小。但是也有少数编译器会被放大到一个int的大小。现在看下面的结果:

class Empty{};  // 空类
class HoldAnInt1
{
private:
    int x;
    Empty e;
};
class HoldAnInt2 : private Empty
{
private:
    int x;
};

现在做以下执行:

可以看到,由于字节对齐的问题,如果包含一个空白类的变量,那么就会变成8个字节(编译器自动对齐了四个字节)。

但是如果我们采取的是继承方式,那么就不会计算这个空白基类的大小,只有自己的一个Int的大小。

这对于一个特别在意空间大小的时候是一个很好的解决方法。EBO一般在单一继承的时候才可行,多重继承是不行的。

实际上你可能会问,怎么会写出一个空类呢?真正的空类并不是什么都没有,可能有一大堆的typedef,enums,static成员等。

作者总结

private继承意味is-implemented-in-terms-of。它通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

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

MI,即Multiple Inheritance,多重继承。 一旦涉及到多重继承,C++社群就分为两个阵营:一方认为如果单一继承是好的,那么多重继承一定更好。另一方认为,单一继承是好的,多重继承则不值得拥有。

一、多重继承的缺点

调用歧义

如果一个类的两个基类拥有同一个函数名呢?那编译器会带一个调用歧义的错误提示。

class B1
{
public:
    bool checkOut() const;
    ...
};
class B2
{
public:
    void checkOut() const;
    ...
};
Class D : public B1,public B2
{
    ...
};

D d;
d.checkOut();

这个调用我们不会知道他到底是调用哪一个checkOut。除非我们指定它:

d.B2::checkOut();

重复的数据拷贝

上面这是一个菱形继承。现在思考一个问题:Input和Output各自继承了File类的一个FileName。然后IOFile再从两个基类中各自继承了这个FileName,那么IOFile就有两份同样的数据了。

解决菱形继承数据重复的问题就是使用virtual inheritance方法。

即:

class Input : virtual public File
{
    ...
};
class Output : virtual public File
{
    ...
};
class IOFile : public Input,public OutFile
{
    ...
};

这样继承的话就只有一份数据拷贝了。

Q:那是不是所有的public继承都用virtual呢?

显然是不行的。使用vitual的体积一般比non-virtual继承的体积更大,访问virtual base class的成员变量速度也更慢。使用virtual继承是需要付出代价的!

事实上,使用复合+继承的方式同样也可以完成菱形继承的功能。只要我们把Input和Output类型作为自己的成员变量即可。

二、多重继承的正当使用

假如我们需要做一个CPerson类,将Person的信息归纳起来,现在翻找我们的库,有个IPerson类和一个PersonInfo类。

class IPerson
{
public:
    virtual string name() const = 0;
    virtual string birthDay() const = 0;
    ...
};
class PersonInfo
{
public:
    virtual char *theName() const;
    virtual char *birthDay() const;
    virtual char *valueDelimOpen() const;
    ...
};

我们的CPerson函数可以利用这两个已有的类,直接就完成我们的功能,这个时候采用多重继承就是一个合适的使用。

作者总结

多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。

virtual继承会增加大小,速度,初始化(以及赋值)复杂度等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。

多重继承的确有正当用途。其中一个情节设计“pubic继承某个interface class”和“private继承某个协助实现的class”的两相结合。

posted @ 2018-09-25 16:30  _NewMan  阅读(267)  评论(0编辑  收藏  举报