Effective C++ 改善程序与设计的55个具体做法笔记与心得 6

六. 继承与面向对象设计

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

‌‌‌‌  public inheritance(公开继承)意味"is-a"(是一种)的关系。

请记住
‌‌‌‌  “public继承”意味is-a。适用于base classes身上的每一件事一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

解释

‌‌‌‌  “public继承”是指派生类(derived class)会继承基类(base class)的公共(public)和保护(protected)属性/方法,实现了is-a关系,即“是一个”的概念。

‌‌‌‌  这里的“is-a”关系,可以理解为类的继承是一种特殊性的关系。例如,“小猫(Cat)”继承自“动物(Animal)”类,我们就可以说,“小猫是动物”(Cat is an Animal),"Cat"类是"Animal"类的一个特例,但同样也是动物。

‌‌‌‌  这里的"每一件事"指的是基类的所有公共和保护成员(比如函数,数据成员等),"适用于"是指这些成员在派生类中也同样可以使用。

‌‌‌‌  因此,当我们说"派生类是基类的一个特例",我们可以将基类中的方法和属性应用于派生类,而且派生类还可以扩展或者覆盖基类的方法和属性。

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

请记住

  • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

解释
‌‌‌‌  这段话讲的是当基类(base classes)和派生类(derived classes)中存在同名的成员时,在派生类中使用这个名字,会优先考虑派生类的成员,从而"遮掩"了基类中的同名成员。

‌‌‌‌  比如我们有一个基类Base,它有一个成员函数func(),然后我们有一个派生类Derived继承自Base,它也有一个成员函数func(),那么在Derived类中直接使用func(),实际上调用的是Derived类中的func()。

‌‌‌‌  这就是"derived classes内的名称会遮掩base classes内的名称"的意思。而"在public继承下从来没有人希望如此",是因为我们通常希望能访问到基类中的成员,这样的设计可以提高代码的复用性,而不是只使用派生类的成员,忽视了基类的成员。

‌‌‌‌  那么,如何解决这种名称遮掩的问题呢?我们可以使用"using声明式"或者"转交函数"。

  1. 使用"using声明式":在派生类中使用"using 基类名::成员名",即可让基类中被遮掩的成员名称在派生类中可见。
class Derived : public Base {
    using Base::func; // 让基类Base中的func()在Derived中可见
};
  1. “转交函数”:在派生类中定义一个新的函数,其内部调用基类的同名函数。这样做实际上是在派生类中提供了一个"接口",通过这个接口可以间接调用到基类的函数。
class Derived : public Base {
public:
    void baseFunc() {
        Base::func(); // 转交函数,调用基类的func()
    }
};

‌‌‌‌  这样,即使派生类Derived内部有一个同名的func()函数,我们也可以通过调用baseFunc()来访问基类Base中的func()函数。

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

请记住

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体指定接口继承。
  • 简朴的impure virtual函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

解释
‌‌‌‌  关于这个问题,先来定义一些术语:

  • 接口继承:派生类继承基类的成员函数的名称和参数。这是继承的基础,这样派生类就能使用和基类一样的方式进行操作。

  • 实现继承:派生类继承基类的成员函数的具体实现。这样,派生类就可以无需重新实现函数的功能,或者可以在基类实现的基础上进行扩展。

然后我们再来看下面这三种函数:

  1. 纯虚函数(pure virtual function):在基类中只有声明没有实现,要求派生类必须实现这个函数。这是一种纯粹的接口继承,因为派生类只知道函数的名称和参数,但是不知道函数应该如何实现,必须自己提供实现方式。

  2. 非纯虚函数(impure virtual function):在基类中既有声明又有一种缺省(默认)的实现,派生类可以选择是否重新实现这个函数。这是一种接口继承加上缺省实现继承,因为派生类既知道函数的名称和参数,也知道一种可能的实现方式。但是,如果缺省实现不满足派生类的需求,派生类可以提供自己的实现方式。

  3. 非虚函数(non-virtual function):在基类中有声明和实现,要求派生类继承这个实现,不能重新实现这个函数。这是一种强制性的接口继承加上强制性的实现继承,因为派生类既知道函数的名称和参数,也必须沿用基类的实现方式,不能提供自己的实现方式。

‌‌‌‌  在public继承下,派生类总是继承基类的接口,就是指无论基类是什么样的函数定义,都会被派生类继承。然后具体的实现方式,就要看这些函数具体是纯虚函数,非纯虚函数,还是非虚函数了。

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

请记住

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

解释

  1. NVI(Non-Virtual Interface)手法:这是一种设计模式,通常应用于基类的设计。在基类中,我们将一些函数声明为public非虚函数(non-virtual),这些函数的实现通常会调用一些私有虚函数(private virtual)。这样,当派生类继承基类时,可以重新定义这些私有虚函数来改变基类函数的行为。这确保了基类的接口(即public非虚函数)是不变的,而实现(即私有虚函数)可以在派生类中变化。NVI手法是Template Method设计模式的一种特殊形式。

  2. Strategy Design Pattern:又称策略模式,是一种行为设计模式,它允许你在运行时改变对象的行为。它通过定义一系列算法,并将每一个算法封装起来,使得它们可以互换使用。

  3. 关于"机能从成员函数移到class外部函数",可以理解为将类的一部分功能实现为非成员函数,也就是在类的外部实现。但是,这样做的一个缺点是非成员函数无法直接访问类的私有或者保护成员,这需要类提供public接口来实现。

  4. tr1::function对象是一个通用多态函数包装器。tr1::function的实例可以存储、复制和调用任何的Callable目标–函数、lambda表达式、bind表达式,或者其他函数对象,还有指向成员函数指针和指向数据成员指针。可以理解这类对象的行为就像一般函数指针。这样的对象可以接收所有与给定目标签名式兼容的可调用物。即,只要你的函数、函数对象、Lambda表达式之类的可以被调用,那么它们就可以被tr1::function类型的变量所持有,并通过这个tr1::function对象来进行调用。

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

‌‌‌‌  non-virtual函数如B::mf和D::mf都是静态绑定,但virtual确实动态绑定。

请记住
‌‌‌‌  绝对不要重新定义继承而来的non-virtual函数。

解释

‌‌‌‌  这是C++的一条重要设计原则,我们来一步一步解析。

‌‌‌‌  首先了解一下虚函数(virtual function)和非虚函数(non-virtual function)。在C++类中,我们可以声明虚函数,并在这个类的派生类中重新定义(重写)这个虚函数,这就是多态的基础。也就是说,当我们通过基类指针或引用对虚函数进行操作时,会调用到派生类的实现。

‌‌‌‌  然而,对于非虚函数来说,就没有这种动态绑定的特性。如果我们重新定义了继承自基类的非虚函数,那么在通过基类指针或引用调用这个函数时,仍然会调用基类中的实现,而不是派生类中的新实现。这可能会导致一些错误,因为我们常常期望调用的是派生类的实现。

‌‌‌‌  那么为什么有时候会出现重定义非虚函数的情况呢?这通常是程序员不小心造成的,或者是程序员不清楚虚函数和非虚函数的区别,尝试在派生类中重写基类的非虚函数。这样的代码可能能够编译通过,但在运行时却无法按照预期工作。

‌‌‌‌  因此,"绝对不要重新定义继承而来的non-virtual函数"这条建议的含义就是,为避免可能的错误和混淆,我们应当避免在派生类中重新定义继承自基类的非虚函数。

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

请记住

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

解释
‌‌‌‌  C++中的缺省参数和虚函数都是非常重要的特性。让我们一起来理解这句话。

‌‌‌‌  首先,缺省参数,是指在函数声明时给参数设定的默认值。这个默认值在函数调用时会被用到,当我们在调用函数时没有为该参数提供实参的情况下。

‌‌‌‌  然后,虚函数,是C++实现多态性的一个关键。通过声明虚函数,我们可以在基类的指针或引用上调用派生类的函数实现。这就是动态绑定,也是多态性的体现。

‌‌‌‌  然后问题来了,如果我们在派生类中试图重新定义一个来自基类的、带有默认参数的虚函数会怎么样?事实上,虽然虚函数是动态绑定的,但默认参数却是静态绑定的。也就是说,如果你的函数调用是通过基类的引用或指针进行的,那么哪怕这个引用或指针实际上指向的是派生类的对象,被调用的函数的默认参数值,还是会使用在基类中定义的那个。

‌‌‌‌  因此,如果你试图在派生类中改变虚函数的默认参数,可能会导致混乱和错误。因为你可能期望使用派生类新定义的默认值,但实际上却是基类中的默认值被使用了。

‌‌‌‌  所以,“绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——确实动态绑定。” 这句话的含义就是,虽然虚函数可以(也应该)在派生类中被覆写,但我们却不应尝试去覆写它的默认参数值,因为在函数调用时,总是基类中的默认参数值被使用,这就和虚函数的动态绑定特性相矛盾了,可能会带来问题。

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

请记住

  • 复合的意义和public继承完全不同。
  • 在应用域,复合意味着has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

解释

‌‌‌‌  在面向对象语言中,有两种主要的代码复用方法:组合(复合)和继承。每种方式都有其特定的意义和用途。

  1. 复合(Composition):又称为组合,是一种"has-a"关系,也就是说,一个类中对象包含了其他类的对象。例如,我们可以说"汽车(Car)有一个引擎(Engine)",这里的"有一个"就体现了复合关系。复合优点在于,可以保持类的封装完整性,因为组合的类和被组合的类之间是相互独立的,在实现上可以更加灵活。

    在实现域来看,复合意味着"is-implemented-in-terms-of"(根据…实现出),也就是说,一个类的实现是根据其他类来完成的。比如你可以通过使用类List的不同方法来实现一个Stack类,这样Stack就是根据List来实现的。

  2. 公开继承(Public Inheritance):这是一种"is-a"关系,子类继承父类的属性和方法,并可以添加新的属性和方法或者重写父类的方法。公开继承意味着一种真实的子类型关系,表示超类的任何地方都可以使用子类来替代。这种关系通常在"物是什么"(What something is)而不是"物有什么"(What something has)的情况下使用。比如,“猫”是一种“动物”,所以我们可以通过公开继承来建立“猫”和“动物”之间的关系。

所以,从这个角度理解,复合和公开继承在语义和应用上是完全不同的。复合更多地反映了"has-a"的关系和"is-implemented-in-terms-of"的实现方法,而公开继承则反映了"is-a"的关系。选择使用哪种方式取决于你在设计代码时所面临的具体问题和需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值