04.继承与面向对象设计

继承与面向对象设计

条款32:确定你的pubilc继承塑模出is-a关系

条款32强调使用公共继承(public inheritance)时,需要确保所使用的继承能够正确地塑模出"is-a"关系。这意味着子类对象应该能够被视为基类对象的一种特殊类型。

下面是对该条款的理解和一些建议:

  1. 理解"is-a"关系:在公共继承中,子类继承了基类的接口和行为,并且可以通过基类指针或引用进行操作。这意味着子类对象应该能够被视为基类对象的一种特殊类型,即子类对象应该可以替代基类对象使用,并且在行为和使用方式上与基类对象相似。

  2. 遵循Liskov替换原则:Liskov替换原则指出,子类对象应该能够替换基类对象并且不会破坏程序的正确性。也就是说,在使用基类对象的地方,如果用子类对象替换后程序仍然正确运行,则说明公共继承的"is-a"关系得到了正确地塑模。

  3. 注意虚函数的重写:在公共继承中,子类可以重写基类的虚函数以改变其行为。但是,需要确保重写的虚函数在行为上与基类的虚函数是一致的,并且满足Liskov替换原则。这样,在使用基类指针或引用操作子类对象时,能够获得预期的行为。

  4. 避免破坏类的不变式:在子类中进行继承时,需要避免破坏基类所定义的类不变式(class invariants)。类不变式是描述类对象状态约束的规则,继承关系应该遵循这些规则,以确保对象的一致性和正确性。

  5. 慎重使用非虚继承:非虚继承(non-virtual inheritance)==用于处理多继承时的菱形继承问题,但它不适用于"is-a"关系的建立。==非虚继承更适用于表示某种组合关系(has-a)而不是"is-a"关系,因此在公共继承中应慎重使用非虚继承。

总结来说,条款32的目标是确保使用公共继承能够正确地塑模出"is-a"关系,即子类对象能够替代基类对象并且在行为和使用方式上与基类对象相似。遵循Liskov替换原则、注意虚函数的重写、避免破坏类的不变式等原则,可以帮助我们正确使用公共继承并避免问题。

请记住:

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

条款33:避免遮掩继承而来的名称

条款33强调避免在派生类中遮掩(hide)继承而来的名称。当派生类中定义了与基类相同名称的成员函数或成员变量时,这些新定义的成员会遮掩基类中相同名称的成员,使其在派生类中不可见。

以下是对该条款的理解和一些建议:

  1. 名称遮掩的问题:当派生类中定义了与基类相同名称的成员时,派生类的成员会隐藏(hide)基类的成员。这可能导致代码中的名称冲突和不一致,使得基类的成员在派生类中不可见。

  2. 使用作用域解析运算符:如果在派生类中需要访问基类中被遮掩的成员,可以使用作用域解析运算符(::)来显式指定所属的作用域。例如,可以使用Base::foo()来调用基类中的foo()函数。

  3. 重载与隐藏的区别:派生类中的函数重载(overload)是指在派生类中定义了与基类同名但参数列表不同的成员函数。**重载不会导致基类的同名函数被遮掩,而是构成了函数重载集。**只有当函数名和参数列表完全相同时,才会发生名称遮掩。

  4. 使用using声明:如果希望在派生类中保留基类中的同名成员的可见性,可以使用using声明来引入基类的名称。使用using Base::foo;可以使基类中的foo在派生类中可见,并且可以直接调用。

  5. 使用转交函数(function forwarding):

    在派生类中定义一个与基类同名的函数,这个函数将被外部调用。

    在派生类的同名函数中,调用基类的同名函数,可以使用作用域解析运算符(::)来显式指定基类的作用域。

    以下是一个示例代码:

    class Base {
    public:
        void foo() {
            // 基类的函数实现
        }
    };
    
    class Derived : public Base {
    public:
        void foo() {
            Base::foo();  // 转交函数,调用基类的同名函数
            // 派生类的其他操作
        }
    };
    

    在上述示例中,派生类Derived中的foo函数转交了基类Base中的foo函数的调用,即在派生类的foo函数中先调用基类的foo函数,然后再执行派生类的其他操作。

总结来说,条款33的目标是避免在派生类中遮掩继承而来的名称,以避免名称冲突和不一致性。通过使用作用域解析运算符、重载和using声明、转交函数等技术手段,可以解决名称遮掩的问题,确保基类的成员在派生类中仍然可见并且可用。这样可以提高代码的可读性和可维护性。

请记住:

derived classes 内的名称会遮掩base classes 内的名称。在public 继承下从来没有人希望如此。

为了让被遮掩的名称再见天日,可使用using 声明式或转交函数(forwarding functions)。

条款34:区分接口继承和和实现继承

这个条款的核心思想是,继承应该被用于两种不同的目的:接口继承和实现继承。这两种继承方式有着不同的目标和应用场景。

  1. 接口继承(Interface Inheritance):

    • 接口继承指的是从基类中继承纯虚函数,即只有函数签名而没有实际实现的函数。
    • 接口继承的目的是创建一个抽象的基类,用于定义一组规范或接口,而不关注具体的实现细节。
    • 派生类必须实现基类中的纯虚函数,以便成为一个具体的类。
    • 通过接口继承,可以实现多态,允许将派生类对象赋值给基类指针或引用,并在运行时调用正确的实现。
  2. 实现继承(Implementation Inheritance):

    • 实现继承指的是从基类继承具有实际实现的函数。
    • 实现继承的目的是通过基类的共同实现来重用代码。
    • 实现继承可以将基类的功能直接继承到派生类中,但它会将基类和派生类紧密地耦合在一起,可能导致较高的继承层次结构复杂性。
    • 使用实现继承时,派生类在某种程度上是基类的"特例",它拥有基类的所有功能,并可能添加自己的特定实现。

请记住:

  • 接口继承和实现继承不同。在public 继承之下,derived classes 总是继承base classes 的接口。
  • 声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口。
  • 声明简朴的(非纯)impure virtual 函数的目的,是让derived classes 继承该函数的接口和缺省实现。
  • 声明non-virtual 函数的目的是为了令derived classes 继承函数的接口及一份强制性实现,non-virtual 函数代表的意义是不变性凌驾特异性,所以它绝不该在derived class 中被重新定义。

条款35:考虑virtual 函数以外的其他选择

这个条款的目的是引导开发者在使用虚函数之前,先考虑一些替代方案。

虚函数允许在基类中声明函数,并允许派生类重写该函数以实现特定行为。然而,使用虚函数可能会引入一些开销,如虚表指针(vptr)和虚表(vtable),以及运行时的动态绑定。在某些情况下,这些开销可能是不必要的或不适用的。

在条款35中,Meyers提供了一些替代虚函数的选择:

  1. 将函数声明为非虚函数:

    • 如果确定某个函数在派生类中不需要被重写,可以将其声明为非虚函数。
    • 这样做可以避免虚函数带来的运行时开销,并且使编译器能够进行更多的优化。
  2. 使用非虚函数接口(non-virtual interface,NVI)手法来实现模板方法模式(Template Method Pattern):

    这一基本设计,也就是“令客户通过public non-virtual 成员函数间接调用 private virtual 函数”,称为 non-virtual interface (NVI)手法。它是所谓Template Method 设计模式(与C++ templates 并无关联)的一个独特变现形式。侯捷把这个non-virtual 函数称为virtual 函数的外覆器(wrapper)。

    • 这种方法利用了C++的多态性和访问控制机制,允许派生类在模板方法中实现自己的行为。

    • 模板方法模式是一种设计模式,其中基类定义了一个模板方法(template method),该方法定义了算法的骨架,但允许派生类实现其中的一些步骤。

    • 模板方法模式通过将可变的部分交给派生类来实现,从而提供了一种替代虚函数的方法。

    • 下面是通过非虚函数接口手法实现模板方法模式的步骤:

      1. 定义基类(抽象类):首先,定义一个基类,它包含一个公共的非虚函数接口方法,用于定义模板方法的结构。该方法通常称为模板方法。
      class Base {
      public:
          void templateMethod(/* 参数列表 */) {
              // 通用的处理代码...
      
              // 调用派生类实现的虚函数
              doSomething(/* 参数列表 */);
      
              // 通用的处理代码...
          }
      
      private:
          virtual void doSomething(/* 参数列表 */) = 0; // 纯虚函数,由派生类实现
      };
      
      1. 派生类实现:派生类继承自基类,并实现基类中的纯虚函数,提供自己的行为。
      class Derived : public Base {
      private:
          void doSomething(/* 参数列表 */) override {
              // 派生类的特定实现...
          }
      };
      
      // 可以定义更多的派生类,每个派生类提供自己的行为...
      
      1. 使用模板方法:创建基类或派生类的实例,并调用模板方法。
      Base* obj = new Derived(); // 使用基类指针指向派生类对象
      
      obj->templateMethod(/* 参数列表 */); // 调用模板方法
      

      通过使用非虚函数接口(NVI)手法,基类中的模板方法可以定义算法的结构,同时调用派生类实现的非虚函数。这样,派生类可以在模板方法中实现自己的行为,从而实现了模板方法模式。非虚函数接口手法通过将派生类的实现封装在非虚函数中,将多态性限制在基类的内部,并提供更好的控制和灵活性。

      需要注意的是,NVI手法并不是C++中的特定功能或语言特性,而是一种设计模式实践。它利用了C++中的多态性和访问控制机制,将模板方法模式应用于具体的类设计中。

  3. 使用函数对象(Function Objects):

    • 函数对象是可调用对象,可以像函数一样使用。它们可以通过重载operator()来实现多态行为。
    • 使用函数对象可以避免虚函数的开销,并提供更大的灵活性和可扩展性。
  4. 藉由Function Pointers 实现策略模式(Strategy Pattern):

    • 策略模式是一种设计模式,其中算法被封装在不同的策略类中,并通过基类指针或引用在运行时选择合适的策略。

    • 使用策略模式可以避免虚函数的开销,并提供更大的灵活性和可维护性。

    • 下面是通过函数指针实现策略模式的步骤:

      1. 定义函数指针类型:首先,定义一个函数指针类型,它与策略函数的签名相匹配。
      using StrategyFunctionPtr = void (*)(/* 参数列表 */);
      
      1. 定义策略函数:然后,实现具体的策略函数,这些函数与策略函数指针的签名匹配。
      void ConcreteStrategy1(/* 参数列表 */) {
          // 具体策略1的实现...
      }
      
      void ConcreteStrategy2(/* 参数列表 */) {
          // 具体策略2的实现...
      }
      
      // 定义更多的具体策略函数...
      
      1. 使用函数指针存储策略:声明一个函数指针变量,并根据需要,将不同的策略函数的地址赋值给它。
      StrategyFunctionPtr strategy; // 声明函数指针变量
      
      // 选择具体策略1
      strategy = &ConcreteStrategy1;
      
      // 选择具体策略2
      strategy = &ConcreteStrategy2;
      
      1. 执行策略:通过调用函数指针,执行选定的策略函数。
      strategy(/* 参数列表 */); // 执行选定的策略函数(具体策略1或具体策略2)
      

      使用函数指针可以实现策略模式的动态选择和执行。通过将不同的策略函数的地址赋值给函数指针,可以在运行时根据需要选择适当的策略。这种方法提供了一种简单而有效的策略模式实现方式,但需要注意函数指针的签名匹配。

      需要注意的是,使用函数指针实现策略模式可能存在一些限制,例如无法存储具有不同签名的策略函数,以及难以处理需要状态或上下文的策略。在这种情况下,使用函数对象(如前面提到的std::function)可能更加灵活和方便。

  5. 藉由tr1::function完成Strategy模式

    提及的tr1::function是指C++ Technical Report 1(TR1)中引入的函数对象类型。在当前的C++标准(C++11及以后),std::function是相应的功能。

    理解通过std::function(或tr1::function)来完成策略模式,可以采用以下步骤:

    1. 定义策略接口:首先,定义一个策略接口(或基类),其中包含定义所有策略对象都必须实现的纯虚函数。
    class StrategyInterface {
    public:
        virtual void execute() = 0;
        // 其他纯虚函数或接口方法...
    };
    
    1. 实现具体策略:接下来,为每个具体的策略实现一个类,这些类派生自策略接口,并提供自己的实现。
    class ConcreteStrategy1 : public StrategyInterface {
    public:
        void execute() override {
            // 具体策略1的实现...
        }
    };
    
    class ConcreteStrategy2 : public StrategyInterface {
    public:
        void execute() override {
            // 具体策略2的实现...
        }
    };
    
    // 可以定义更多的具体策略类...
    
    1. 使用std::function存储策略:使用std::function来存储不同策略对象的可调用实例。这样可以实现运行时的策略选择。
    #include <functional>
    
    std::function<void()> strategy; // 使用std::function存储策略对象
    
    // 选择具体策略1
    strategy = []() {
        ConcreteStrategy1 strategy1;
        strategy1.execute();
    };
    
    // 选择具体策略2
    strategy = []() {
        ConcreteStrategy2 strategy2;
        strategy2.execute();
    };
    
    1. 执行策略:通过调用std::function中存储的策略对象,执行相应的策略。
    strategy(); // 执行选定的策略(具体策略1或具体策略2)
    

    使用std::function(或tr1::function)可以方便地存储不同策略对象,并在运行时选择和执行特定的策略。通过使用函数对象类型,可以实现策略模式的灵活性和可扩展性,而无需修改现有的策略接口和具体策略类。

    请注意,自C++11起,使用std::function更为常见和推荐,而非tr1::function

请记住:

  • virtual 函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method 设计模式。

  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。

  • tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

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

这个原则强调了在继承关系中对于非虚函数的使用和重写的限制。

理解条款36的关键点如下:

  1. 非虚函数与虚函数的区别:非虚函数是在基类中定义的普通成员函数,而虚函数是在基类中定义且使用virtual关键字声明的函数。虚函数支持运行时的动态绑定,而非虚函数则使用静态绑定。

  2. 非虚函数的继承行为:当派生类继承一个非虚函数时,基类中的非虚函数会在派生类中保持相同的行为,不会被派生类重新定义或覆盖。

  3. 危险的重新定义:重新定义(override)一个继承而来的非虚函数是危险的行为。这是因为在继承链中,通过基类指针或引用调用非虚函数时,实际调用的是静态类型(基类)的函数,而不是动态类型(派生类)的函数。这可能导致意外的行为和不一致的结果。

  4. 虚函数的重写:与非虚函数不同,派生类可以通过在派生类中重新定义(override)虚函数来改变其行为。这样,当通过基类指针或引用调用虚函数时,会根据实际的动态类型调用正确的函数。

总而言之,条款36建议避免重新定义继承而来的非虚函数,以防止意外的行为和不一致性。如果需要改变继承而来的函数的行为,应该将该函数声明为虚函数,并在派生类中进行重写。这样可以确保正确的多态行为,并遵循Liskov替换原则(Liskov Substitution Principle)的规定。

请记住:

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

Liskov替换原则是什么?

Liskov替换原则(Liskov Substitution Principle)是面向对象设计的一个重要原则,由计算机科学家Barbara Liskov提出。该原则指导着正确和合理地使用继承和多态性,以确保软件系统的可靠性和可扩展性。

Liskov替换原则可以简要描述为:

“如果S是T的子类型,那么在不破坏程序的正确性的前提下,任何程序中的T类型的对象都可以被替换为S类型的对象。”

换句话说,子类型(派生类)应该能够替换其基类型(基类)在任何程序中的使用,而不会引入错误或破坏程序的行为。这个原则的目标是保持代码的一致性、可维护性和可扩展性。

遵循Liskov替换原则的关键要点如下:

  1. 子类型的行为:子类型(派生类)应该完全符合其基类型(基类)的约定、协议和合约。即,子类型应该实现基类型的所有接口,并保持相同的行为和预期结果。

  2. 不破坏基类型的不变量:子类型的操作和行为不应破坏基类型的不变量、约束和规范。即,子类型不应该放宽基类型的前置条件、加强后置条件或改变基类型的约束。

  3. 不破坏客户端代码:子类型的使用不应该强制客户端代码进行特定的类型检查或假设。即,客户端代码应该能够以基类型的方式使用子类型对象,而不需要做任何额外的处理。

遵循Liskov替换原则有助于构建稳健和可靠的软件系统。它鼓励设计合理的继承关系,正确使用多态性,并确保代码的一致性和可扩展性。同时,它也提供了一种指导原则,帮助开发者评估和调整继承关系中的设计选择和决策。

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

这个原则强调了在继承关系中对于缺省参数的使用和重新定义的限制。

理解条款37的关键点如下:

  1. 缺省参数值的作用:在函数声明中,可以为参数提供缺省参数值,这样在调用函数时,如果没有提供相应参数的值,将使用缺省参数值作为参数的值。这使得函数调用更加灵活和简洁。

  2. 缺省参数值的定义位置:缺省参数值是在函数的声明或定义中指定的,并不是在函数的调用处指定的。因此,在函数声明或定义中指定的缺省参数值在整个继承层次结构中保持不变。

  3. 重新定义缺省参数值的危险性:当派生类重新定义(override)一个继承而来的带有缺省参数值的函数时,如果重新定义的函数中提供了不同的缺省参数值,会导致继承链中的函数调用出现二义性和不一致的结果。

  4. 缺省参数值的使用原则:为了避免重新定义缺省参数值带来的问题,应该在继承关系中避免重新定义带有缺省参数值的函数。如果需要不同的缺省参数值,可以通过重载(overloading)或使用函数重写(function overriding)来实现。

总而言之,条款37建议避免重新定义继承而来的带有缺省参数值的函数,以防止二义性和不一致性的问题。如果需要不同的缺省参数值或参数列表,应该使用重载或函数重写的方式来实现。这样可以确保函数调用的一致性和可预测性,并遵循正确的函数重载和继承的原则。

请记住:

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

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

这个条款强调了使用复合而不是继承来构建对象之间的关系的重要性。

理解条款38的关键点如下:

  1. 继承与复合的区别:继承是一种对象之间的关系,其中派生类继承了基类的特性和行为。而复合是一种对象之间的关系,其中一个对象包含另一个对象作为其一部分。

  2. has-a关系:has-a关系表示一个对象具有另一个对象作为其一部分的关系。例如,一个汽车has-a引擎,一个公司has-a员工。这种关系通常可以通过复合来实现。

  3. “根据某物实现出”关系:有时,一个类可以通过复合另一个类来实现其功能。这意味着类的实现依赖于另一个类的实现。例如,一个集合类可以“根据某物实现出”迭代器功能,即通过复合一个迭代器对象来实现迭代功能。

  4. 复合的优势:相比继承,复合具有更大的灵活性和可定制性。通过复合,可以将对象之间的关系限制在需要的范围内,并避免继承链的复杂性和脆弱性。复合还可以提供更好的封装和模块化,使对象之间的依赖关系更清晰。

总而言之,条款38鼓励使用复合来构建对象之间的关系,特别是has-a关系或“根据某物实现出”关系。通过复合,可以实现更灵活、可定制和可维护的代码。继承仍然是一种有用的工具,但在设计对象之间的关系时,应该优先考虑使用复合。

请记住:

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

条款39:明智而审慎地使用private继承

理解条款39的关键点如下:

  1. 继承关系的访问控制:C++中的继承关系可以通过不同的访问控制符(public、protected、private)来指定对基类成员的访问权限。其中,private继承将基类的成员作为派生类的私有成员。

  2. private继承的含义:使用private继承意味着派生类从基类那里继承了实现细节(implementation details),而不是接口(interface)。这种继承形式用于实现派生类“is-implemented-in-terms-of”基类的关系。

  3. private继承的适用情况:private继承通常用于以下情况:

    • 当派生类需要重新定义(override)基类的虚函数,以提供自己的实现。
    • 当派生类需要访问基类的保护成员,以在派生类中实现特定行为。
    • 当派生类需要实现某种特定的接口或协议,基类提供了一些实现细节,但不需要对外公开。
  4. 使用private继承的注意事项:在使用private继承时,需要注意以下事项:

    • private继承并不表示“是一个”(is-a)关系,而是一种实现关系。
    • private继承不应被滥用,应该慎重考虑是否需要使用它。
    • private继承应该与组合(composition)相比较,确保选择最适合的关系来实现设计需求。

总之,条款39建议在使用private继承时要明智而审慎。它强调了private继承的特殊含义和适用情况,并提醒开发者在选择继承关系时要仔细考虑,并与组合关系进行比较和评估。正确地使用private继承可以提供灵活性和实现的便利性,同时保持良好的设计和代码的可维护性。

请记住:

  • Private继承意味is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)地级别低。但是当derived class 需要访问protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合(composition)不同,private继承考研造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

条款40:明智而审慎地使用多重继承

理解条款40的关键点如下:

  1. 多重继承的含义:多重继承是C++中一种允许一个派生类从多个基类继承特性和行为的机制。派生类可以获得多个基类的成员和接口。
  2. 多重继承的优势:多重继承可以在某些情况下提供灵活性和复用性。它可以使派生类具有不同基类的特性,以实现更丰富和复杂的功能。
  3. 多重继承的适用情况:多重继承通常用于以下情况:
    • 当派生类需要从多个基类中继承不同的接口和行为时。
    • 当派生类需要在多个不相关的类之间共享代码和数据。
  4. 使用多重继承的注意事项:在使用多重继承时,需要注意以下事项:
    • 显示指定基类的构造函数和析构函数。
    • 虚拟继承(virtual inheritance)可以解决菱形继承(diamond inheritance)问题。
    • 菱形继承问题(diamond inheritance problem):当派生类通过多条路径继承同一个基类时,可能导致二义性和冗余。

正确地使用多重继承可以提供灵活性和功能复用,但也需要处理潜在的二义性和冗余问题。

请记住:

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承地需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes 不带任何数据,将是最具使用价值的情况。
  • 多重继承的确有正当途径。其中一个情节涉及“public”继承某个Interface class“和”private继承某个协助实现的class“的两相结合。
  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值