《Effective C++》精简总结——32~40条款:继承与面向对象设计


条款32,38,39应当结合进行解读理解;

条款33是为了维护 public 继承关系或 private 定制继承基类函数接口而提出的补充内容;

条款34说明了对于 base class 的成员函数/函数接口,如何设定其 virtual 性;

条款35在条款34的基础上,提出了 virtual 接口的替代实现方案;

条款36,37是关于c++静态绑定和动态绑定的内容;

条款39,40讲述了派生类应以何方式声明继承其基类;



六、继承与面向对象设计


32. public 继承 = “是一个” (is-a)

Public inheritance means “is-a”.

  • public 继承意味者 is-a。适用于 base classes 身上的每一件事也一定适用于 derived classes 身上,因为每一个 derived classes 对象也都是一个 base class 对象。反之则不然,也即 base classes 是 derived classes 的一般化描述;
  • is-a 并非是唯一存在于 classes 之间的关系。另两个常见的关系是 has-a(有一个,条款38)和 is-implemented-in-terms-of(根据某物实现出,条款39)。上述两个关系很容易被误塑为 is-a ,而造成设计错误,导致代码通过编译但是行为与预设不符;

33. 避免遮掩继承而来的名称

Avoid hiding inherited names.

  • c++内含名称遮掩规则(name-hiding rules),其作用是:用内层/局部作用域的名称遮掩掉外围/全局作用域的名称。 又可分为两种场景,一是函数区块中的名称遮掩,二是继承当中的名称遮掩,主要讨论第二种场景;
  • c++会对继承而来的名称进行缺省遮掩,换句话说就是 derived classes 内的名称可能会遮掩 base classes 内的名称。这是因为 derived class 在继承了 base class 之后,若是对继承而来的函数进行了重载,则c++的内涵名称遮掩规则对其进行了遮掩,而这违反了31条款中派生类 is-a 基类的关系定义,所以须以推翻(override);
  • 取消c++对继承而来的名称进行缺省遮掩的方式有两种:
    ①public继承下为原本会被遮掩的名称(即声明而不是函数定义式)引入一个 using 声明,否则被重载的基类的名称会被掩盖(即原先的名称/函数不再在派生类中体现/可用);在public继承下,基类的public和protected成员都应对派生类可见,所以使用using声明式可以使得继承而来的某给定名称所有同名函数在派生类中可见,解决了违反is-a关系的问题;
    ②private继承下使用inline转交函数(forwarding function)(inline函数见条款30: inline函数剖析,即在类内定义),将特定基类public函数转交给派生类。 正如①中所说,using声明式会导致基类中所有public和protected名称对于派生类可见,但是对于private继承而言往往只想要继承名称中的某一特定版本,此时using声明式不符合需求,所以使用转交函数。如想继承Base中的无参版本mf1(),则语句为virtual void mf1() { Base::mf1(); }。同时,对于老旧编译器,是将继承而来的名称加入派生类作用域中的方式;
  • 此外,对于派生类所继承的基类是模板基类的情况,仿佛也是遇到了名称遮掩问题,实则不是,需要加以区分。详见 条款43: 派生类访问其模板基类内的名称

34. 区分接口继承和实现继承

Differentiate between inheritance of interface and inheritance of implementation.

  • 普通函数继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现;
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以,如果不实现多态,不要把函数定义成虚函数;
  • 接口继承和实现继承是不同的。在 public 继承下,derived class 总是继承 base classes 的接口,那么若是能在基类施加的函数,对派生类施加同样是合理的。但是 base classes 设计者可以决定是否继承基类已有的接口实现;
  • 对于 base classes 的接口/成员函数(通常是public声明的),分为 pure virtualsimple(impure) virtualnon-virtual 接口三种类型。它们之间对于 derived classes 实现的影响各不相同,影响的大小是依次递增的;
  • 声明一个 pure virtual 函数让 derived classes 只继承函数接口。 且在派生类中必须实现该接口的行为;
  • 声明一个 impure virtual 函数让 derived classes 继承该函数的接口和缺省实现。 对于函数接口和缺省实现的继承方式,可分为两种,但总是遵循接口和缺省实现分开的惯例,声明部分表现接口,定义部分表现缺省行为
    ①将函数接口设为 pure virtual函数,将缺省实现设为 non-virtual 的(通常为非 public 的),并在派生类需要使用该缺省实现的时候做 inline 调用;
    ②利用 pure virtual 函数必须在 derived classes 中重新声明,但也可自行实现的事实(对于纯虚函数,其可以在类外进行定义实现,详见 纯虚函数也可定义 )。在纯虚函数的实现中实现缺省行为,在派生类中 inline 调用该缺省行为或重载该缺省行为;
  • 声明一个 non-virtual 函数让 derived classes 继承函数的接口并继承基类的强制性实现。该声明的使用场景通常是该接口所表现出的不变性(invariant)凌驾于其特异性(specialization);
  • 需要避免两个 class 设计错误:
    ①将所有函数声明为 non-virtual。使得派生类没有空间进行特化(定制化实现,重载等);
    ②将所有函数声明为 virtual。有些函数不应当拥有特化的空间,即其不变性胜过其特异性;
  • 相关内容也可见 C++ override 关键字用法

35. 考虑 virtual 函数以外的其它选择

Consider alternatives to virtual functions.

  • 当为解决成员函数继承/多态实现…问题时候,并不一定非要使用 virtual 函数技巧,可以考虑如下的替代方案:

    ①使用 non-virtual interface(NVI) 手法,其是 Template Method 设计模式的一种特殊形式。以 public non-virtual 成员函数的包裹较低访问访问性的 private 或 protected 声明的 virtual 成员函数,因此又称为 virtual 的外覆器(wrapper);

    ②将 virtual 函数替换为“函数指针成员变量”,这是 Strategy 设计模式的一种分解表现形式。例子可见 《剑指Offer》21:调整数组顺序使奇数位于偶数前面:c++中的函数指针转函数参数。该方法将功能实现从成员函数移至 class 外部,有无法访问到对象 non-public 成员的缺点;

    ③以 tr1::function 成员变量替换 virtual 函数,因而允许使用任何可调用物(callable entity)(类似仿函数?)搭配一个兼容于需求的签名式(即函数适配器,使用bind(),包含在<function>头文件中,可见:STL适配器详解),这也是 Strategy 设计模式的某种形式。tr1::function 对象的行为就像一般函数指针,这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调物;

    ④将继承体系内的 virtual 函数替换为另一个继承体系中的 virtual 函数。这是 Strategy 设计模式的传统实现方法;

36. 绝不重新定义继承而来的 non-virtual 函数

Never redefine an inherited non-virtual function.

  • 条款7:为多态基类(polymorphic base classes)声明 virtual 析构函数,是本条款的一个特例,因为若是按照本条款,基类的析构函数是 non-virtual 的,那么对于派生类的自有成员则难以释放和析构,造成局部销毁,资源泄漏的情形;
  • 若是对继承而来的 non-virtual 函数进行重载,则 base::mf 和 derived::mf (mf:member function)都是静态绑定的,所以当 指向 derived 类型对象的 pointer 或 reference 的声明式为 base 或 derived 时,会分别调用 base::mf 或 derived::mf,造成同一函数在继承体系中类的行为不一致的问题,违反了条款31:public继承关系是 is-a;相反的,virtual 函数则是动态绑定的,不会存在该问题。相关知识见: 关于 c++ 动态绑定和静态绑定;
  • 由以上,绝不重新定义继承而来的 non-virtual 函数

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

Never redefine a function’s inherited default parameter value.

  • 正如 c++ 动态绑定和静态绑定 中后半部分所说,c++ 对于 virtual 函数是动态绑定的,对于缺省参数是静态绑定的
  • 若是重新定义继承而来的缺省参数值,则对于静态类型是基类的指针/引用,而动态类型是派生类的情况,会出现所执行的成员函数是派生类的实现版本,但所用缺省参数是基类所指定的奇怪组合;
  • 这一特性是 c++ 实现中对于效率的妥协所致(详细说明见以上超链接),所以 绝对不要重新定义一个继承而来的缺省参数值
  • 对于既遵守不重新定义继承而来的缺省参数值,同时又想要给派生类提供来自基类的缺省参数值,可以使用 NVI 手法(条款35中有提到),可将 virtual函数 的缺省值设置问题转化为 non-virtual函数 的缺省值设置,即 private virtual函数 负责具体实现,non-virtual函数 指定缺省参数,且 non-virtual 函数不应被派生类重新定义保证了不重新定义缺省参数。当需要修改缺省参数设置时,只需更改基类中 virtual 函数的 wrapper函数 的缺省参数即可,代码具有低相依性(low dependencies);

38. 通过复合实现新类型 (has-a | is-implemented-in-terms-of)

Model “has-a”(有一个) or “is-implemented-in-terms-of”(根据某物实现出) through composition.

  • 复合(composition)是类型间关系的一种,即某种类型的对象内含其它种类型对象的情形。当复合发生于应用域(application domain)内的对象之间,表现出有一个的关系;当复合发生于实现域(implementation domain,实现的数据结构,资源等)表现的是根据某物实现出的关系;
  • 复合:has-a / is-implemented-in-terms-of
    public 继承: is-a

39. 明智审慎地使用 private 继承(is-implemented-in-terms-of)

Use private inheritance judiciously.

  • 首先先给出基类派生类的继承关系表:
类成员/继承方式public 继承protected 继承private 继承
基类的 public 成员派生类的 public 成员派生类的 protected 成员派生类的 private 成员
基类的 protected 成员派生类的 protected 成员派生类的 protected 成员派生类的 private 成员
基类的 private 成员在派生类中不可见在派生类中不可见在派生类中不可见
  • private 继承的规则
    ① 编译器不会将一个 derived class 对象转换为一个 base class 对象;
    ② 由 private base class 继承而来的所有成员,无论其在 base class 中是 public 还是 protected 的,都会变成 private 的;
  • 让派生类 private 继承基类,意为派生类采用基类的实现,而不采用基类的接口
  • 尽可能使用复合,必要时才使用 private 继承;
  • private 继承意味着 is-implemented-in-terms-of。它通常比复合的级别低。但是当派生类需要访问 protected base class 的成员,或需要重新定义继承而来的 virtual 函数时,一般采用private继承来达到 is-implemented-in-terms-of 目的
  • EBO(empty base optimization,空白基类最优化),其一般只在单一继承下才可行。常见的应用就是基类不带任何数据,也就是所谓的 empty classes,STL 中就有该种 classes 的实现,比如 unary_function、binary_function(见 STL仿函数:2. 仿函数能够进行配接的条件),这些时用户自定义函数对象(仿函数)的通常会继承的 classes;
  • 和复合不同,private 继承可以使得 empty base 最优化。这对于致力于“对象尺寸最小化”的开发者而言,很重要;

40. 明智审慎地使用多重继承

Use multiple inheritance judiciously.

  • 多重继承(MI,multiple inheritance)和单一继承(SI,single inheritance)。多重继承,若基类们中有相同名称的成员或成员函数,则会造成调用歧义,对于此情况,c++解析时进行匹配,找出最佳匹配后才检验可取用性,若是没有最佳匹配,会造成编译器报错,除非指明所调用的内容,才可以消除该歧义;
  • 多重继承比单一继承复杂。可能导致新的歧义性,以及对 virtual 继承的需要;
  • 对于菱形/钻石型多重继承,可见:C++类空间大小,同时关注其中的内存对齐问题
  • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具实用价值的情况
  • 非必要不使用 virtual bases,平常使用 non-virtual 继承。如果必须使用 virtual base classes,尽可能避免在其中放置数据(避免自己处理初始化);
  • 多重继承的确有正当用途。其中一个场景为"public 继承某个 Interface class"和"private 继承某个协助实现的 class"的两相组合,也即是实现 interface class,见 条款31中实现 Interface class 两种最常见机制
  • 对于菱形继承,c++中也有例子:
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值