• Effective C++ 规则39:明智而谨慎的使用private继承


    1. class Person { //... };
    2. class Student : private Person { //... };
    3. void eat(const Person& p);
    4. void study(const Student& s);
    5. void main()
    6. {
    7. Person p;
    8. Student s;
    9. eat(p);
    10. eat(s);
    11. }

            这个时候,eat(s)就会报错,提示不允许对不可访问的基类Person进行转换。所以,可以看出,如果采用 private 继承:
           1)编译器不会自动将一个 derived class 转换为一个 base class;
           2)原来在 base class 中的所有 protected or public 成员,被 private 继承到 derived class 后,在derived class 中都会变成 private 属性;

    • private 继承意味着 is-implemented-in-terms-of
    • 如果 class D 以 private 方式继承了 class B,你的用意应该是希望使用 class B 中已经实现了的某些功能,也就是说,D 对象是根据 B 对象实现而来的,除此之外,没有其他意图。

            前面一个条款说到复合(composition)也有 is-implemented-in-terms-of 的含义。那就是是选用 composition 还是 private 继承呢?答案是尽量使用 compostion,必要时才使用 private 继承。必要的情况有两种:

            1)当有 protected 成员或者 virtual 函数牵扯进来时

            假设现在有一个Widget类,我们需要让这个类记录每个成员函数的被调用次数,然后在运行期间周期性的审查记录的信息。为此,我们需要设定一个计时器。而且我们发现在现有的库函数中,Timer class 可以满足需求:

    1. class Timer
    2. {
    3. public:
    4. explicit Timer(int tickFrequency);
    5. virtual void onTick() const;
    6. //...
    7. };

            其中的 virtual onTick() 函数就定义的时钟每滴答一次,就需要执行的动作,非常符合我们的需求。此时,为了让 Widget 类重写 Timer 内的 virtual 函数,Widget 必须继承 Timer。public 继承在此时并不恰当,因为 Widget is not a Timer。采用 public 继承,会让 Widget 继承 Timer 的不必要的接口,容易造成客户不正确使用 Widget 接口。所以此时,我们必须以 private 形式继承 Timer:

    1. class Widget : private Timer
    2. {
    3. private:
    4. virtual void onTick() const;
    5. };

            通过 private 继承,既能够重新定义 virtual onTIck() 函数,又能够使得原有的Timer class 中的 public 接口隐藏起来,不对客户开放,保持了接口的不变性。 
            上面的设计已经可以较好的达到功能需求,但是别忘了,我们说过,如果 private 继承非必要,尽量采用 composition 的方式实现。对于上面的例子,也可以采用 composition 方式实现:

    1. class Widget
    2. {
    3. private:
    4. class WidgetTimer : public Timer
    5. {
    6. public:
    7. virtual void onTick() const;
    8. //...
    9. };
    10. WidgetTimer timer;
    11. //...
    12. };

            这种设计虽然复杂一些,但是可以有两个理由让你选择 composition 方式:

    • 如果你想设计 Widget ,使得它拥有 derived classes,但是你同时又想阻止 derived class 重新定义 onTick。如果 Widget 继承了 Timer,即使是 privete 继承,上面的想法也不能够实现。但是如果 WidgetTimer 是 Widget 内部的一个 private 成员并继承了 Timer,derived Widget class 将无法使用 WidgetTimer,当然也就无法继承或重新定义它的 virtual 函数。
    • 降低了编译依存性(见这篇文章)。如果 Widget 继承了 Timer,那么必须 #include "Timer.h",那么 Widget 所在文件和 Timer.h 文件就存在编译依赖性。但如果采用 composition 方式,WidgetTimer 可以移出到 Widget 之外而 Widget 内部含有一个指向 WidgetTimer 的指针,Widget 可以只带一个 WidgetTimer 的声明就好了,不需要再 #include 任何与 Timer 有关的东西。

            2)面对大小为零的独立对象时

            如果现在有一个不含有数据成员的 empty class 和一个含有它的其他类:

    1. class Empty { };
    2. class HoldsAnInt
    3. {
    4. private:
    5. int x;
    6. Empty e;
    7. };

            你会发现,sizeof(HoldsAnInt) > sizeof(int),一个不含有数据成员的类居然还是需要内存;这是因为对于大多数编译器,在面对大小为 0 的独立(非附属)对象时,C++ 官方要求在背后安插一个 char 类型数据到空对象中。所以你会发现,sizeof(Empty) = 1。同时呢,由于对齐要求,HoldsAnInt 类还会加上一些 padding,导致最后 sizeof(HoldsAnInt ) 可能就等于 8 了。
    但是,如果你继承一个 empty class,上面所说的就不成立了:

    1. class HoldsAnInt : private Empty
    2. {
    3. private:
    4. int x;
    5. };

            这个时候,几乎就可以确定 sizeof(HoldsAnInt) == sizeof(int),这就是所说的 emty base optimization。如果你比较在意空间,这种 private 继承方式可能就是一种更好的选择。 

  • 相关阅读:
    el-table 动态合并单元格和给某一行添加颜色
    赋能千百行加快 “5G+工业互联网”落地深耕
    Zabbix小实验
    数据库事务四大特性-ACID(原子性、一致性、隔离性、持久性)
    0019Java程序设计-SSM + MySQL 家庭医生预约平台
    dvwa靶机与web漏洞扫描(vega)
    《一个程序猿的生命周期》-《发展篇》- 42.逃离“管理”陷阱
    这几个小众的软件,我只告诉你
    《痞子衡嵌入式半月刊》 第 92 期
    【2018年数据结构真题】
  • 原文地址:https://blog.csdn.net/xiaoan08133192/article/details/127856418