Effective C++条款39:继承与面向对象之(明智而审慎地使用private继承)

一、先介绍一下private继承的语法

  • 某派生类private继承于基类之后:
    • ①基类中的所有内容(不论是public、protected、private)在派生类中都是不可访问的
    • ②不能再将派生类对象转换为基类对象
    • ③基类仍然可以重写/隐藏基类的成员方法

二、private继承意味着什么关系?

  • private继承意为implemented-terms-of(根据某物实现出):
    • 假设你让class D以private继承于class B,用意为采用class B内的某些特性来实现class D再无其他意义了
    • 借助条款34的属于:private继承意味只有实现部分(也就是基类中已经实现的函数)被继承,接口部分(基类中只定义还未实现的)应该被省去
  • 与类的复合模式的关系:
    • 在条款38中介绍了类的复合(composition)模式,其中类的复合也有着“is-implemented-in-terms-of”的意义
    • 两者有着同样的意义,但是建议:尽可能使用复合,必要时才使用private继承
    • 何时才必要使用private呢?
      • 主要是当protected成员和/或virtual函数牵扯进来的时候。当派生类想要访问基类的protected成分或者基类想要重写一个或多个virtual函数
      • 还有一种情况,就是当空间方面的利害关系足以踢翻private继承的支柱时(下面介绍)

三、演示案例

  • 假设我们有这样一个需求:
    • 我们有一个类Widget,现在想要知道Widget成员函数被调用的次数
    • 现在我们绝对修改Widget class,让它记录每个成员函数被调用次数
    • 为了完成这项工作,我们定义了下面的定时器,该定时器每隔一段时间就会对数据进行统计(统计每个成员函数被调用的次数)
class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次
};
  • 现在我们需要:
    • Timer类会根据设置的频率进行滴答前进,每次滴答就调用virtual函数
    • 现在我们需要Widget类继承于Timer,然后重新定义这个virtual函数,然后使用该函数来统计Widget的数据(统计每个成员函数被调用的次数)

①错误的做法:以public方式继承

  • 错误的做法是让Widget以public方式继承于Timer,然后重写其virtual函数
class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次
};

class Widget :public Timer {
public:
    virtual void onTick()const;
};
  • 为什么错误:
    • 类似于is-a的关系:我们知道Widget不是一个Timer,我们总不该对一个Widget调用onTick()函数吧?这样会显示很诡异
    • 另外,会违反条款18的忠告:让接口容易被正确使用,不易被误用

②以private方式继承

  • 为了完成上面的需求,我们可以让Widget以private的方式继承于Timer
  • 代码如下:
class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次
};

class Widget :private Timer {
private:
    virtual void onTick()const; //查看Widget的数据..等等
};
  • 我们在Wiget中重写了onTick()函数,但是将其声明为private的,不能将其声明为public接口,因为如果声明为public,又再次与上面的public继承相类似了

③以复合的形式实现

  • 在条款38我们介绍过,复合形式的类也有着“is-implemented-in-terms-of”的意义,因此我们还可以使用复合模式来实现这个功能
  • 代码如下:
class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次
};

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

  • 我们派生了一个Timer的派生类WidgetTimer,并重写onTick()函数,然后定义一个WidgetTimer类对象定义于Widget class中
  • 相同的问题,建议使用复合模式,而不建议使用private继承,原因有两点:
    • ①防止Widget的派生类重写onTick()函数:
      • 在继承方式下:如果Wiget又定义了派生类,你不希望派生类去重写onTick()函数,但是这种情况可能会无法阻止
      • 在复合模式下:Widget的派生类就不可能有机会去重写onTick()函数了,因为WidgetTimer类是Widget内部的一个private成员,派生类永远无法访问
    • ②可以将Widget的编译依存性降至最低:
      • 在继承方式下:如果Widget继承与Timer,那么当Widget被编译时需要知道Timer的定义(不仅仅是声明),因此你可能会在Widget的头文件中包含#include "Timer.h"这样的东西
      • 在复合模式下:假设我们修改上面的复合模式,将WidgetTimer定义在Widget之外,然后在Widget内定义一个WidgetTimer的指针,此时Widget可以只带着WidgetTimer的声明式,那么当Widget编译时就不需要任何与Timer的任何东西。对大型系统而言,这是很重要的措施

四、private继承的另外一个使用场景

  • private继承还用于:基类的class不带有任何数据时
  • 基类通常:没有non-static成员变量,也没有virutal函数(因为这种函数的存在会为每一个对象带来一个vptr,见条款7),也没有virtual base classes(因为这样的base classes也会导致类的体积增大,见条款40)

演示案例

  • 当一个类没有任何成员变量时,编译器会自动为其设定大小(不同的编译器不同),通常为1字节(C++默认安插一个char到空类中)。例如:
class Empty {}; //空类

sizeof(Empty);  //1字节
  • 上面的规则是适用于独立的类,如果其有派生类,并且派生类有成员,那么这种规则就会消失。例如:
class Empty {};

class HoldsAnint :private Empty {
private:
    int x;
};

sizeof(HoldsAnint); //4
  • 这个规则就是所谓的EBO:空白积累最优化(empty base optimization)。如果你的程序非常在意空间,那么值得注意EBO
  • 另外还需要注意:EBO一般只在单一继承(而非多重继承下)才可行
  • 例如STL有很多实现中就用到了empty classes。例如unary_func和binary_function等。EBO使继承很少增加派生类的大小

五、总结

  • private继承意为“is-implemented-in-terms-of(根据某物实现出)”。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计时合理的
  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要
发布了1525 篇原创文章 · 获赞 1085 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104835285