6. 继承和面向对象设计

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

  1. “public继承”意味is-a
  2. is-a并非是唯-存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。
  3. “public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

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

  1. base class内的某物(也许是个成员函数、typedef、或成员变量)时,编译器可以找出我们所指涉的东西,因为derived classes继承了声明于base classes内的所有东西。实际运作方式是,derived class作用域被嵌套在base class作用域内
  2. 有时候你并不想继承base classes的所有函数,这是可以理解的。
    1. 在public继承下,这绝对不可能发生,因为它违反了public继承所暗示的“base和derived classes之间的isa关系”。(这也就是为什么上述using声明式被放在derived class的public区域的原因:base class内的public名称在publicly derived class内也应该是public。)
    2. 然而在private继承之下(见条款39)它却可能是有意义的。例如假设Derived以private形式继承Base,而Derived唯想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。不,我们需要不同的技术,即一个简单的转交函数(forwarding function)
  3. inline转交函数(forwarding function)的另一个用途是为那些不支持using声明式(注:这并非正确行为)的老旧编译器另辟一条新路,将继承而得的名称汇入derived class作用域内。
  4. derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  5. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)

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

  1. public继承由函数接口继承和函数实现继承
  2. 身为class设计者,有时候你会希望
    1. derived classes只继承成员函数的接口(也就是声明);
    2. 有时候你又会希望derived classes同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;
    3. 又有时候你希望derived classes同时继承函数的接口和实现,并且不允许覆写任何东西。
1
2
3
4
5
6
7
8
9
class Shape{
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
};

class Rectangle: public Shape{};
class Ellipse: public Shape{};
  1. Shape是个抽象class;:它的pure virtual函数draw使它成为一个抽象class。所以客户不能够创建Shape class的实体,只能创建其derived classes的实体。尽管如此,Shape还是强烈影响了所有以public形式继承它的derived classes,因为:
    1. 成员函数的接口总是会被继承。pure virtual函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。把这两个性质摆在一起,你就会明白:
    2. 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
    3. 令人意外的是,我们竟然可以为pure virtual函数提供定义。也就是说你可以为Shape::draw供应一份实现代码,C++并不会发出怨言,但调用它的唯一途径是“调用时明确指出其class名称”:
    4. 可以为简朴的impure virtual 函数提供更平常更安全的缺省实现
  2. 简朴的impure virtual函数背后的故事和pure virtual函数有点不同。一如往常,derived classes继承其函数接口,但impure virtual函数会提供一份实现代码,derivedclasses可能覆写(override)它。
  3. 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
  4. 可以“提供缺省实现给derived classes,但除非它们明白要求否则免谈”。此间技俩在于切断“virtual函数接口”和其“缺省实现”之间的连接。
1
2
3
4
5
6
7
8
9
10
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
缺省行为,将飞机飞至指定目的地
}
  1. 若想使用缺省实现(例如ModelA和ModelB),可以在其fly函数中对defaultFly做一个inline调用
1
2
3
4
class ModelA: public Airplane{
public:
virtual void fly(const Airport& destination) {defaultFly(destination)}
};
  1. 现在Modelc class不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:
1
2
3
4
5
6
7
8
9
class ModelC: public Airplane{
public:
virtual void fly(const Airport& destination);
};

void ModelC:fly(const Airport& destination)
{
将C型飞机飞至指定的目的地
}
  1. pure virtual函数必须在derived classes中重新声明,但它们也可以拥有自己的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
};

void Airplane::fly(cosnt Airport& destiantion)
{
缺省行为
}

class ModelA: public Airplane{
public:
virtual void fly(const Airport& destination)
{
Airplane::fly(destination);
}
};

class ModelC: public Airplane{
public:
virtual void fly(const Airport& destination);
};

void ModelC::fly(const Airport& destination)
{
飞指定地点
}
  1. 如果成员函数是个non-virtual函数,意味是它并不打算在derived classes中有不同的行为。实际上一个non-virtual成员函数所表现的不变性(invariant),凌驾其特异性(specialization),因为它表示不论derived class变得多么特异化,它的行为都不可以改变。就其自身而言:
    1. 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
  2. pure virtual函数、simple(impure)virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。
  3. 第一个错误是将所有函数声明为non-virtual。.这使得derived classes没有余裕空间进行特化工作。
  4. 如果你关心virtual函数的成本,请容许我介绍所谓的80-20法则(也可见条款30)。这个法则说,一个典型的程序有80号的执行时间花费在20%的代码身上。此一法则十分重要,因为它意味,平均而言你的函数调用中可以有80号是virtual而不冲击程序的大体效率。所以当你担心是否有能力负担virtual函数的成本之前,请先将心力放在那举足轻重的20号代码上头,它才是真正的关键。
  5. 另一个常见错误是将所有成员函数声明为virtual。有时候这样做是正确的,例如条款31的Interface classes。然而这也可能是class设计者缺乏坚定立场的前兆。某些函数就是不该在derived class中被重新定义,果真如此你应该将那些函数声明为non-virtual。.没有人有权利妄称你的class适用于任何人任何事任何物而他们只需花点时间重新定义你的函数就可以享受一切。如果你的不变性(invariant)凌驾特异性(specialization).,别害怕说出来。
  6. 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  7. pure virtual函数只具体指定接口继承。
  8. 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
  9. non-virtual函数具体指定接口继承以及强制性实现继承。

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

  1. 藉由Non-Virtual Interface手法实现Template Method模式
  2. 直接在class定义式内呈现成员函数,那也就让它们全都暗自成了inline。
  3. 这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C+templates并无关联)的一个独特表现形式。
  4. NVI手法的一个优点隐身在上述代码注释“做一些事前工作”和“做一些事后工作”之中。那些注释用来告诉你当时的代码保证在“virtual函数进行真正工作之前和之后”被调用。
  5. 在NVI手法下其实没有必要让virtual函数一定得是private。某些class继承体系要求derived class在virtual函数的实现内必须调用其base class的对应兄弟,而为了让这样的调用合法,virtual函数必须是protected,不能是private。有时候virtual函数甚至一定得是public(例如具备多态性质的base classes的析构函数一见条款7),这么一来就不能实施NVI手法了。
  6. 藉由Function Pointers实现Strategy模式
  7. 同一人物类型之不同实体可以有不同的健康计算函数。

  1. 某己知人物之健康指数计算函数可在运行期变更。
  2. 实际上任何时候当你将class内的某个机能(也许取道自某个成员函数)替换为class外部的某个等价机能(也许取道自某个non-member non-friend函数或另一个cass的non-friend成员函数),这都是潜在争议点。
  3. 唯一能够解决“需要以non-member函数访问class的non-public成分”的办法就是:弱化class的封装。运用函数指针替换virtual函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期改变计算函数”)是否足以弥补缺点(例如可能必须降低GameCharacter封装性),是你必须根据每个设计情况的不同而抉择的。
  4. 藉由trl:function完成Strategy模式
  5. 一日习惯了templates以及它们对隐式接口(见条款41)的使用,基于函数指针的做法看起来便过分苛刻而死板了。
  6. 如果我们不再使用函数指针(如前例的healthFunc),而是改用一个类型为tr1::unction的对象,这些约束就全都挥发不见了。就像条款54所说,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象、或成员函数指针),只要其签名式兼容于需求端。
1
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
  1. tr1::function像一般的函数指针,这个tr1::function类型产生的对象可以持有任何与此签名式兼容的可调用物。所谓兼容,就是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。

  1. 古典的Strategy模式

  1. 本条款的根本忠告是,当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案。下面快速重点复习我们验证过的几个替代方案:
  2. 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。
  3. 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
  4. 以trl:function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
  5. 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。
  6. virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NⅥ手法自身是一个特殊形式的Template Method设计模式。
  7. 将机能从成员函数移到clss外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  8. trl:function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

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

  1. 绝对不要重新定义继承而来的non-virtual函数。

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

  1. 继承一个带有缺省参数值的virtual函数
  2. virtual函数系动态绑定,而缺省参数值却是静态绑定。静态绑定又各前期绑定,early binding;动态绑定又名后期绑定,late binding。
  3. virtual函数是动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值
  4. 条款35列了不少virtual函数的替代设计,其中之一是NVI(non-virtual interface)手法:令base class内的一个public non-virtual函数调用private virtual函数,后者可被derived classes重新定义。这里我们可以让non-virtual函数指定缺省参数,而private virtual函数负责真正的工作:

  1. 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数一你唯一应该覆写的东西一却是动态绑定。

38. 通过符合塑模出has-a或根据某物实现出

  1. 复合(composition)这个术语有许多同义词,包括la四yering(分层),containment(内含),aggregation(聚合)和embedding(内嵌)。
  2. 当复合发生于应用域内的对象之间,表现出hasa的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系
  3. 比较麻烦的是区分is-a(是一种)和is-implemented-in-terms.-of(根据某物实现出)这两种对象关系。
  4. 复合(composition)的意义和public继承完全不同。
  5. 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain.),复合意味is-implemented-.in-terms-of(根据某物实现出)。

39. 明智而审慎地使用private继承

  1. 如果classes之间的继承关系是private,编译器不会自动将一个derived class对象(例如Student)转换为一个base class对象(例如Person)。这和public继承的情况不同。
  2. 第二条规则是,由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。
  3. Private继承意味is-implemented-in-terms-of(根据某物实现出),这个事实有点令人不安,因为条款38才刚指出复合(composition)的意义也是这样。你如何在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用private继承。何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候。其实还有一种激进情况,那是当空间方面的利害关系足以踢翻private继承的支柱时。

  1. 用复合取而代之,在Widget内声明一个嵌套式private class,后者以public形式继承Timer并重新定义onTick,

  1. 稍早我曾谈到,private继承主要用于“当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分,或为了重新定义一或多个virtual函数”。但这时候两个classes之间的概念关系其实是is-implemented-in-terms-of(根据某物实现出)而非is-a。然而我也说过,有一种激进情况涉及空间最优化,可能会促使你选择“private继承”而不是“继承加复合”。
  2. 这个激进情况真是有够激进,只适用于你所处理的clss不带任何数据时。这样的classes没有non-static成员变量,没有virtual函数(因为这种函数的存在会为每个对象带来一个ptr,见条款7),也没有virtual base classes(因为这样的base classes也会招致体积上的额外开销,见条款40)。于是这种所谓的empty classes对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小,所以如果你这样做:

  1. 几乎可以确定sizeof(HoldsAnInt)=sizeof(int)。这是所谓的EBO(empy base optimization; 空白基类最优化),我试过的所有编译器都有这样的结果。如果你是一个程序库开发人员,而你的客户非常在意空间,那么值得注意EBO。另外还值得知道的是,EBO一般只在单一继承(而非多重继承)下才可行,统治C++对象布局的那些规则通常表示EBO无法被施行于“拥有多个base”的derived classes身上。
  2. 现实中的"empty"classes并不真的是empty。虽然它们从未拥有non-static成员变量,却往往内含typedefs,enums,.static成员变量,或non-virtual函数。STL就有许多技术用途的empty classes,其中内含有用的成员(通常是ypedefs),包括base classes unary_function和binary function,这些是“用户自定义之函数对象”通常会继承的classes.感谢EBO的广泛实践,使这样的继承很少增加derived classes的大小。
  3. 尽管如此,让我们回到根本。大多数classes并非empy,所以EBO很少成为private继承的正当理由。更进一步说,大多数继承相当于isa,这是指public继承,不是private继承。复合和private继承都意味is-implemented-.in-terms-of,但复合比较容易理解,所以无论什么时候,只要可以,你还是应该选择复合。
  4. 当你面对“并不存在is-a关系”的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能成为正统设计策略。即便如此你也已经看到,一个混合了public继承和复合的设计,往往能够释出你要的行为,尽管这样的设计有较大的复杂度。“明智而审慎地使用private继承”意味,在考虑过所有其他方案之后,如果仍然认为private继承是“表现程序内两个classes之间的关系”的最佳办法,这才用它。
  5. Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  6. 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

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

  1. 多重继承(multiple inheritance;ML),单一继承(single inheritance;SI)
  2. 当MI进入设计景框,程序有可能从一个以上的base classes继承相同名称(如函数、typedef等等)。那会导致较多的歧义(ambiguity)
  3. 注意此例之中对checkOut的调用是歧义(模棱两可)的,即使两个函数之中只有一个可取用(一个类内的checkOut是public,另一个类内的却是private)。这与C++用来解析(resolving)重载函数调用的规则相符:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配函数后才检验其可取用性。本例的两个checkOuts有相同的匹配程度(译注:因此才造成歧义),没有所谓最佳匹配.因此ElectronicGadget::checkOut的可取用性也就从未被编译器审查。
  4. 为了解决这个歧义,你必须明白指出你要调用哪-个base class内的函数:
1
mp.BorrowableItem:checkOut();
  1. 多重继承的意思是继承1个以上的base classes,但这些base classes并不常在继承体系中义有更高级的base classes,因为那会导致要命的“钻石型多重继承”:

  1. 令所有直接继承自它的classes采用“virtual继承

  1. C++标准程序库内含一个多重继承体系,class templates,名称分别是basic_ios, basic_istream, basic_ostream和basic_iostream
  2. 从正确行为的观点看,public继承应该总是virtual。
  3. 规则很简单:任何时候当你使用public继承,请改用virtual public继承.
  4. 为避免继承得来的成员变量重复,编译器必须提供若干幕后戏法,而其后果是:使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。种种细节因编译器不同而异,但基本重点很清楚:你得为virtual继承付出代价。
  5. virtual继承的成本还包括其他方面。支配“virtual base classes初始化”的规则比起non-virtual bases的情况远为复杂且不直观。virtual base的初始化责任是由继承体系中的最低层(nost derived)class负责,这暗示
    1. (1)classes若派生自virtual bases而需要初始化,必须认知其virtual bases一一不论那些bases距离多远,
    2. (2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。
  6. 我对virtual base classes(亦相当于对virtual继承)的忠告很简单。
    1. 第一,非必要不使用virtual bases。平常请使用non-virtual继承。
    2. 第二,如果你必须使用virtual base classes,尽可能避免在其中放置数据。这么一来你就不需担心这些classes身上

  1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  2. virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtualbase classes不带任何数据,将是最具实用价值的情况。
  3. 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。

6. 继承和面向对象设计
http://binbo-zappy.github.io/2024/11/27/effective-cpp/6-继承和面向对象设计/
作者
Binbo
发布于
2024年11月27日
许可协议