《C++高级编程》读书笔记(十:揭秘继承技术)

news2025/1/11 11:06:18

1、参考引用

  • C++高级编程(第4版,C++17标准)马克·葛瑞格尔

2、建议先看《21天学通C++》 这本书入门,笔记链接如下

  • 21天学通C++读书笔记(文章链接汇总)

1. 使用继承构建类

1.1 扩展类

  • 当使用 C++ 编写类定义时,可以告诉编译器,该类继承(或扩展)了一个已有的类。通过这种方式,该类将自动包含原始类的数据成员和方法
    • 原始类称为父类、基类或超类
    • 扩展已有类可以使该类 (现在称为派生类或子类) 只描述与父类不同的那部分内容
  • 在 C++ 中,为扩展一个类,可在定义类时指定要扩展的类,此处使用了名为 Base 和 Derived 的类
    class Base {
    public:
        void someMethod();
    protected:
        int mProtectedInt;
    private:
        int mPrivateInt;
    };
    // Derived 类派生自 Base 类
    // Derived 本身就是一个完整的类,只是刚好共享了 Base 类的特性而已
    // Derived 不一定是 Base 唯一的派生类。其他类也可是 Base 的派生类,这些类是 Derived 的同级类
    class Derived : public Base {
    public:
        void someOtherMethod();
    };
    
1.1.1 客户对继承的看法
  • 在调用某个方法时,使用派生类的代码不需要知道是继承链中的哪个类定义了这个方法
    // 调用了 Derived 对象的两个方法,而其中一个方法是在 Base 类中定义的
    Derived myDerived;
    myDerived.someMethod();
    myDerived.someOtherMethod();
    
    Base myBase;
    myBase.someOtherMethod(); // 错误,继承是单向的
    
  • 继承的运行方式是单向的,Derived 类与 Base 类具有明确的关系,但是 Base 类并不知道与 Derived 类有关的任何信息。这意味着 Base 类型的对象不支持 Derived 类的 public 方法和数据成员,因为 Base 类不是 Derived 类

在这里插入图片描述

  • 指向某个对象的指针或引用可以指向声明类的对象,也可以指向其任意派生类的对象。指向 Base 对象的指针可以指向 Derived 对象,对于引用也是如此。客户仍然只能访问 Base 类的方法和数据成员,但是通过这种机制,任何操作 Base 对象的代码都可以操作 Derived 对象
    Base* base = new Derived();
    // 尽管对象是 Derived 类型,但编译器只是将它看成 Base 类型,而 Base 类型没有定义 someOtherMethod() 方法
    base->someOtherMethod(); // 错误
    
1.1.2 从派生类的角度分析继承
  • 派生类可访问基类中声明的 public、protected 方法和数据成员,但不能访问基类中声明的 private 方法和数据成员
    • 建议将所有数据成员都默认声明为 private
      • 如果希望任何代码都可以访问这些数据成员,就提供 public 的获取器和设置器
      • 如果仅希望派生类访问它们,就提供 protected 的获取器和设置器
    • 把数据成员默认设置为 private 的原因
      • 这会提供最高级别的封装,意味着可改变数据的表示方式而 public 或 protected 接口保持不变。不直接访问数据成员,也可在 public 或 protected 设置器中方便地添加对输入数据的检查
1.1.3 禁用继承
  • C++ 允许将类标记为 final,这意味着继承这个类会导致编译错误
    class Base final {
        //...
    };
    
    class Derived : public Base { // 错误,因为 Base 声明为 final 表示禁用继承
        // ...
    };
    

1.2 重写方法

  • 在许多情况下,可能需要替换或重写某个方法来修改类的行为
1.2.1 将所有方法都设置为 virtual,以防万一
  • 在 C++ 中,重写 (override) 方法有一点别扭,只有在基类中声明为 virtual 的方法才能被派生类正确地重写
    class Base {
    public:
        virtual void someMethod();
    protected:
        int mProtectedInt;
    private:
        int mPrivateInt;
    };
    
  • 即使 Derived 类不大可能扩展,也最好还是将这个类的方法设置为 virtual,以防万一
    class Derived : public Base {
    public:
        virtual void someOtherMethod();
    };
    

根据经验,为避免因为遗漏 virtual 关键字引发的问题,可将所有方法设置为 virtual (包括析构函数,但不包括构造函数)。注意,由编译器生成的析构函数不是 virtual

1.2.2 重写方法的语法
  • 为了重写某个方法,需要在派生类的定义中重新声明这个方法,并在派生类的实现文件中提供新的定义
    // 基类中 someMethod() 方法
    void Base::someMethod() {
        cout << "This is Base's version of someMethod()." << endl;
    }
    
    // 派生类重写 someMethod() 声明
    class Derived : public Base {
    public:
        virtual void someMethod() override; // 添加 override 关键字表示重写
        virtual void someOtherMethod();
    };
    
    // 派生类重写 someMethod() 方法
    void Derived::someMethod() {
        cout << "This is Derived's version of someMethod()." << endl;
    };
    

    一旦将方法或析构函数标记为 virtual,它们在所有派生类中就一直是 vitual,即使在派生类中删除了 virtual 关键字

1.2.3 客户对重写方法的看法
  • 下面的代码与先前一样可以运行,调用 Base 版本的 someMethod()
    Base myBase;
    myBase.someMethod();
    
  • 如果声明一个 Derived 类对象,将自动调用派生类版本的 someMethod()
    Derived myDerived;
    myDerived.someMethod();
    
  • 如果一个对 Base 对象的引用实际引用的是 Derived 对象,调用 someMethod() 实际上会调用派生类版本,如下所示
    Derived myDerived;
    Base& ref = myDerived;
    ref.someMethod(); // 调用 Derived's version someMethod()
    
  • 即使基类的引用或指针知道这实际上是一个派生类,也无法访问没有在基类中定义的派生类方法或成员。下面的代码无法编译,因为 Base 引用没有 someOtherMethod() 方法
    Derived myDerived;
    Base& ref = myDerived;
    myDerived.someOtherMethod(); // 可行
    ref.someOtherMethod(); // 错误
    
  • 非指针或非引用对象无法正确处理派生类的特征信息。可将 Derived 对象转换为 Base 对象,或将 Derived 对象赋值给 Base 对象,因为 Derived 对象也是 Base 对象。然而,此时这个对象将遗失派生类的所有信息
    Derived myDerived;
    Base assignedObject = myDerived;
    assignedObject.someMethod(); // 调用 Base's version someMethod()
    
  • 基类的指针或引用指向派生类对象时,派生类保留其重写方法。但是通过类型转换将派生类对象转换为基类对象时,就会丢失其独有特征。重写方法和派生类数据的丢失称为截断
    • 将 Base 对象当作占据了一块内存的盒子。Derived 对象是稍微大一点的盒子,因为它拥有 Base 对象的一切,还添加了一点内容。对于指向 Derived 对象的引用或指针,这个盒子并没有变:只是可以用新的方法访问它。然而,如果将 Derived 对象转换为 Base对象,就会为了适应较小的盒子而扔掉 Derived 类全部的 “独有特征”
1.2.4 override 关键字
  • 下面 Derived 没有重写 Base 类的 someMethod(),而是创建了一个新的虚方法,可用 override 关键字避免这种情况
    class Base {
    public:
        virtual void someMethod(double d);
    };
    
    class Derived : public Base {
    public:
        virtual void someMethod(int i); // 缺少关键字 override 导致创建虚方法
        // 会导致编译错误,因为 Base 类中 someMethod() 方法只接受 double
        // virtual void someMethod(int i) override; 
    };
    
    Derived myDerived;
    Base& ref = myDerived;
    ref.someMethod(1.1); // 调用 Base's version someMethod()
    
1.2.5 virtual 的真相
隐藏而不是重写
  • 由于这个方法不是 virtual,因此实际没有被重写,相反,Derived 类创建了一个新的方法,名称也是 go(),这个方法与 Base 类的 go() 方法完全没有关系
    class Base {
    public:
        void go() {
            cout << "go() called on Base" << endl;
        };
    };
    
    class Derived : public Base {
    public:
        void go() {
            cout << "go() called on Derived" << endl;
        }; 
    };
    
    Derived myDerived;
    myDerived.go(); // "go() called on Derived"
    
如何实现 virtual
  • 静态绑定(早绑定):C++ 在编译类时,会创建一个包含类中所有方法的二进制对象。在非虚情况下,将控制交给正确方法的代码是硬编码,此时会根据编译时的类型调用方法
  • 动态绑定(晚绑定):如果方法声明为 virtual,会使用名为虚表 (vtable) 的特定内存区域调用正确的实现。每个具有一个或多个虚方法的类都有一张虚表,这种类的每个对象都包含指向虚表的指针,这个虚表包含指向虚方法实现的指针。通过这种方法,当使用某个对象调用方法时,指针也进入虚表,然后根据实际的对象类型执行正确版本的方法
    class Base {
    public:
        virtual void func1() {}
        virtual void func2() {}
        void nonVirtualFunc() {}
    };
    
    class Derived : public Base {
    public:
        virtual void func2() override {}
        void nonVirtualFunc() {}
    };
    
    Base myBase;
    Derived myDerived;
    
  • 下图显示了两个实例虚表的高级视图
    • myBase 对象包含了指向虚表的一个指针,虚表有两项,一项是func1(),另一项是 func2()。这两项指向 Base::func1() 和 Base::func2() 的实现
    • myDerived 也包含指向虚表的一个指针,这个虚表也包含两项,一项是 func1(),另一项是 func2()。myDerived 虚表的 func1() 项指向 Base::func1(),因为 Derived 类没有重写 func1();但是 myDerived 虚表的 func2() 项指向 Derived::func2()
    • 注意两个虚表都不包含用于 nonVirtualFunc() 方法的项,因为该方法没有设置为 virtual
      在这里插入图片描述
虚析构函数的需求
  • 应该将析构函数声明为 virtual,如果析构函数未声明为 virtual,很容易在销毁对象时不释放内存。唯一允许不把析构函数声明为 virtual 的例外情况是:类被标记为 final(禁用继承)
  • 如果在析构函数中什么都不做,只想把它设置为 virtual,可显式地设置 “= default”
    class Base {
    public:
        virtual ~Base() = default;
    };
    
1.2.6 禁用重写
  • C++ 允许将方法标记为 final,这意味着无法在派生类中重写这个方法
    class Base {
    public:
        virtual ~Base() = default;
        virtual void someMethod() final;
    };
    

2. 使用继承重用代码

2.1 WeatherPrediction 类

  • 假定要编写一个简单的天气预报程序,同时给出华氏温度和摄氏温度,WeatherPrediction 类的定义如下
    // WeatherPrediction.h
    #pragma once
    
    #include <string>
    
    class WeatherPrediction {
    public:
        // Virtual destructor
        virtual ~WeatherPrediction();
        
        // Sets the current temperature in Fahrenheit
        virtual void setCurrentTempFahrenheit(int temp);
        
        // Sets the current distance between Jupiter and Mars
        virtual void setPositionOfJupiter(int distanceFromMars);
        
        // Gets the prediction for tomorrow's temperature
        virtual int getTomorrowTempFahrenheit() const;
        
        // Gets the probability of rain tomorrow. 1 means
        // definite rain. 0 means no chance of rain.
        virtual double getChanceOfRain() const;
        
        // Displays the result to the user in this format:
        // Result: x.xx chance. Temp. xx
        virtual void showResult() const;
        
        // Returns a string representation of the temperature
        virtual std::string getTemperature() const;
    
    private:
        int mCurrentTempFahrenheit;
        int mDistanceFromMars;
    };
    

2.2 在派生类中添加与替换功能

  • 定义一个新类 MyWeatherPrediction,这个类从 WeatherPrediction 类继承
  • 支持摄氏温度的第一步是添加新方法,允许用摄氏温度设置当前的温度,从而获取明天以摄氏温度表示的天气预报。还需包含在摄氏温度和华氏温度之间转换的私有辅助方法。这些方法可以是静态方法,因为它们对于类的所有实例都相同
    // MyWeatherPrediction.h
    #pragma once
    
    #include "WeatherPrediction.h"
    
    class MyWeatherPrediction : public WeatherPrediction {
    public:
        // 添加方法
        virtual void setCurrentTempCelsius(int temp);
        virtual int getTomorrowTempCelsius() const;
        // 重写/替换方法
        virtual void showResult() const override;
        virtual std::string getTemperature() const override;
    
    private:
        static int convertCelsiusToFahrenheit(int celsius);
        static int convertFahrenheitToCelsius(int fahrenheit);
    };
    

3. 利用父类

3.1 父类构造函数

  • 对象并不是突然建立起来的,创建对象时必须同时创建父类和包含于其中的对象。C++ 定义了如下创建顺序
    • 如果某个类具有基类,执行基类的默认构造函数。除非在初始化列表中调用了基类构造函数,否则此时调用这个构造函数而不是默认构造函数
    • 类的非静态数据成员按照声明的顺序创建
    • 执行该类的构造函数
    // 通常不建议在类定义中直接实现方法,此处为了方便演示
    class Something {
    public:
        Something() {
            cout << "2";
        }
    };
    
    class Base {
    public:
        Base() {
            cout << "1";
        }
    };
    
    class Derived : public Base {
    public:
        Derived() {
            cout << "3";
        }
    private:
        Something mDataMember;
    };
    
    int main() {
        Derived myDerived;
        return 0;
    }
    
    // 输出结果
    '123'
    
  • 从派生类向基类传递构造函数的参数很正常,但是无法传递数据成员。如果这么做,代码可以编译,但是在调用基类构造函数之后才会初始化数据成员。如果将数据成员作为参数传递给父类构造函数,数据成员不会初始化

虚方法的行为在构造函数中是不同的,如果派生类重写了基类中的虚方法,从基类构造函数中调用虚方法,就会调用虚方法的基类实现而不是派生类中的重写版本

3.2 父类的析构函数

  • 由于析构函数没有参数,因此始终可自动调用父类的析构函数。析构函数的调用顺序刚好与构造函数相反
    • 调用类的析构函数
    • 销毁类的数据成员,与创建的顺序相反
    • 如果有父类,调用父类的析构函数
    class Something {
    public:
        Something() {
            cout << "2";
        }
        virtual ~Something() {
            cout << "2";
        }
    };
    
    class Base {
    public:
        Base() {
            cout << "1";
        }
        virtual ~Base() {
            cout << "1";
        }
    };
    
    class Derived : public Base {
    public:
        Derived() {
            cout << "3";
        }
        virtual ~Derived() {
            cout << "3";
        }
    private:
        Something mDataMember;
    };
    
    int main() {
        Derived myDerived;
        return 0;
    }
    
    // 输出结果
    '123321'
    
  • 将所有析构函数声明为 virtual。编译器生成的默认析构函数不是 virtual,因此应该定义自己(或显式设置为默认)的虚析构函数,至少在父类中应该这么做
  • 与构造函数一样,在析构函数中调用虚方法时,虚方法的行为将有所不同。如果派生类重写了基类中的虚方法,在基类的析构函数中调用该方法,会执行该方法的基类实现,而不是派生类的重写版本

3.3 使用父类方法

  • 在派生类中重写方法时,将有效地替换原始方法。然而,方法的父类版本仍然存在,仍然可以使用这些方法
  • 在 C++ 中,调用当前方法的父类版本是一种常见操作。如果存在派生类链,每个派生类都可能想执行基类中已经定义的操作,同时添加自己的附加功能,示例:书本类型的类层次结构
    • Book 基类有两个虚方法:getDescription() 和 getHeight()
    • 所有派生类都重写了 getDescription(),只有 Romance 类通过调用父类 (Paperback) 的 getHeight(),然后将结果除以 2,重写了 getHeight()
    • Paperback 类没有重写 getHeight(),因此 C++ 会沿着类层次结构向上寻找实现了 getHeight() 的类
    • 在本例中,Paperback::getHeight() 将解析为 Book::getHeight()
    #include <iostream>
    #include <string>
    
    using namespace std;
    
    class Book {
    public:
        virtual ~Book() = default;
        virtual string getDescription() const { 
            return "Book"; 
        }
        virtual int getHeight() const { 
            return 120; 
        }
    };
    
    class Paperback : public Book {
    public:
        virtual string getDescription() const override {
            return "Paperback " + Book::getDescription();
        }
    };
    
    class Romance : public Paperback {
    public:
        virtual string getDescription() const override {
            return "Romance " + Paperback::getDescription();
        }
        virtual int getHeight() const override { 
            return Paperback::getHeight() / 2; 
        }
    };
    
    class Technical : public Book {
    public:
        virtual string getDescription() const override {
            return "Technical " + Book::getDescription();
        }
    };
    
    int main() {
        Romance novel;
        Book book;
        cout << novel.getDescription() << endl; // Outputs "Romance Paperback Book"
        cout << book.getDescription() << endl;  // Outputs "Book"
        cout << novel.getHeight() << endl;      // Outputs "60"
        cout << book.getHeight() << endl;       // Outputs "120"
        
        return 0;
    }
    

在这里插入图片描述

3.4 向上转型和向下转型

  • 用派生类对基类的指针或引用赋值,则不会产生截断,这是通过基类使用派生类的正确途径,也叫作向上转型。这也是让方法和函数使用类的引用而不是直接使用类对象的原因。使用引用时,派生类在传递时没有截断
    Base myBase = myDerived; // 截断
    Base& myBase = myDerived; // 不会截断
    
  • 将基类转换为其派生类也叫作向下转型,仅在必要的情况下才使用向下转型,且应该使用 dynamic_cast(),以使用对象内建的类型信息,拒绝没有意义的类型转换
    • 这种内建信息通常驻留在虚表中,dynamic_cast 运算符只能用于含有虚函数的类之间的转换,因为它需要在运行时检查对象的实际类型。如果在进行类型转换时发生了问题,比如试图将一个指向非多态类型的指针转换为指向派生类类型的指针,程序可能会出现未定义的行为或崩溃
      • 如果针对某个指针的 dynamic_cast() 失败,这个指针的值就是 nullptr,而不是指向某个无意义的数据
      • 如果针对对象引用的 dynamic_cast() 失败,将抛出 std::bad_cast 异常
    // lessPresumptuous() 实际只能用于 Derived 对象,只应接受 Derived 指针
    void lessPresumptuous(Base* base) {
        // 使用 dynamic_cast 运算符将指向 Base 类型对象的指针进行类型转换
        // 得到一个指向 Derived 类型对象的指针 myDerived
        Derived* myDerived = dynamic_cast<Derived*>(base);
        if (myDerived != nullptr) {
            // ...
        }
    }
    

4. 继承与多态性

4.1 设计多态性的电子表格单元格

  • 类和对象中关于 SpreadsheetCell 类的定义,其中单元格可以是 double、string 或其他类型,可以设计多态性的电子表格单元格来实现这一目的
  • 下图显示了 SpreadsheetCell 层次结构具有多态性的方法。由于 DoubleSpreadsheetCell 和 StringSpreadsheetCell 都从同一个父类 SpreadsheetCell 继承,从其他代码的角度看,它们是可以互换的,实际上这意味着
    • 两个派生类都支持由基类定义的同一接口(方法集)
    • 使用 SpreadsheetCell 对象的代码可调用接口中的任何方法,而不需要知道这个单元格是 StringSpreadsheetCell 还是 DoubleSpreadsheetCell
    • 由于虚方法的特殊能力,会根据对象所属的类调用接口中每个方法的正确实例
    • 其他数据结构可通过引用父类类型,包含一组多类型的单元格

在这里插入图片描述

4.2 SpreadsheetCell 基类

4.2.1 初次尝试
  • SpreadsheetCell 基类负责定义所有派生类支持的行为。本例所有单元格都需要将值设置为字符串。此外,所有单元格都需要将当前值返回为字符串。基类定义中声明了这些方法,以及显式设置为默认的虚析构函数,但没有数据成员
    // 这个父类声明了派生类支持的行为,但是并不定义这些行为的实现
    class SpreadsheetCell {
    public:
        virtual ~SpreadsheetCell() = default;
        virtual void set(std::string_view inString);
        virtual std::string getString() const;
    };
    
4.2.2 纯虚方法和抽象基类
  • 纯虚方法在类定义中显式说明该方法不需要定义。如果将某个方法设置为纯虚方法,就是告诉编译器当前类中不存在这个方法的定义。具有至少一个纯虚方法的类称为抽象类,因为这个类没有实例。编译器会强制接受这个事实:如果某个类包含一个或多个纯虚方法,就无法构建这种类型的对象
    • 采用专门的语法指定纯虚方法:方法声明后紧接着 = 0
    • 抽象类提供了一种禁止其他代码直接实例化对象的方法,而它的派生类可以实例化对象
    class SpreadsheetCell {
    public:
        virtual ~SpreadsheetCell() = default;
        virtual void set(std::string_view inString) = 0;
        virtual std::string getString() const = 0;
    };
    
    SpreadsheetCell cell; // 错误,抽象类无法创建类对象
    // 可以成功编译,因为实例化了抽象基类的派生类
    std::unique_ptr<SpreadsheetCell> cell(new StringSpreadsheetCell());
    

4.3 独立的派生类

  • 编写 StringSpreadsheetCell 和 DoubleSpreadsheetCell 类只需要实现父类中定义的功能
    • 想实现并使用字符串单元格和双精度值单元格,因此单元格不应该是抽象的:必须实现从父类继承的所有纯虚方法
    • 如果派生类没有实现从父类继承的所有纯虚方法,那么派生类也是抽象的,就不能实例化派生类的对象
4.3.1 StringSpreadsheetCell 类定义和实现
class StringSpreadsheetCell : public SpreadsheetCell {
public:
    virtual void set(std::string_view inString) override;
    virtual std::string getString() const override;
private:
    // optional 从 C++17 开始定义在 <optional> 头文件中
    // 用于确认是否已经设置了单元格的值
    std::optional<std::string> mValue; // 用于存储实际单元格数据
};
void StringSpreadsheetCell::set(string_view inString) {
    mValue = inString;
}

string StringSpreadsheetCell::getString() const {
    // 使用 std::optional 的 value_or() 方法对此进行简化
    // 如果 mValue 包含实际的值,将返回相应的值,否则将返回空值
    return mValue.value_or("");
}
4.3.2 DoubleSpreadsheetCell 类定义和实现
class DoubleSpreadsheetCell : public SpreadsheetCell {
public:
    virtual void set(double inDouble);
    virtual void set(std::string_view inString) override;
    virtual std::string getString() const override;
private:
    static std::string doubleToString(double inValue);
    static double stringToDouble(std::string_view inValue);

    std::optional<double> mValue;
};
void DoubleSpreadsheetCell::set(double inDouble) {
    mValue = inDouble;
}

void DoubleSpreadsheetCell::set(string_view inString) {
    mValue = stringToDouble(inString);
}

string DoubleSpreadsheetCell::getString() const {
    // 如果未存储任何值,则返回一个空字符串;如果具有值,则使用 value() 方法获取
    return (mValue.has_value() ? doubleToString(mValue.value()) : "");
}

4.4 利用多态性

  • SpreadsheetCell 是个抽象类,因此不能创建这种类型的对象。然而,仍然可以使用 SpreadsheetCell 的指针或引用,因为它实际上指向的是其中一个派生类
  • 当调用 getString() 方法时,不同的对象以不同的方式完成这一任务。StringSpreadsheetCell 返回它存储的值,或返回空字符串。如果包含值,DoubleSpreadsheetCell 首先执行转换;否则返回一个空字符串
    vector<unique_ptr<SpreadsheetCell>> cellArray;
    
    cellArray.push_back(make_unique<StringSpreadsheetCell>());
    cellArray.push_back(make_unique<StringSpreadsheetCell>());
    cellArray.push_back(make_unique<DoubleSpreadsheetCell>());
    
    cellArray[0]->set("hello");
    cellArray[1]->set("10");
    cellArray[2]->set("18");
    
    cout << "Vector values are [" << cellArray[0]->getString() << "," 
                                  << cellArray[1]->getString() << ","
                                  << cellArray[2]->getString() << "]" << endl;
    

5. 多重继承

5.1 从多个类继承

  • 从语法角度看,定义具有多个父类的类很简单,由于列出了多个父类,Baz 对象具有如下特性
    • Baz 对象支持 Foo 和 Bar 类的 public 方法,并且包含这两个类的数据成员
    • Baz 类的方法有权访问 Foo 和 Bar 类的 protected 数据成员和方法
    • Baz 对象可以向上转型为 Foo 或 Bar 对象
    • 创建新的 Baz 对象将自动调用 Foo 和 Bar 类的默认构造函数并按照类定义中列出的类顺序进行
    • 删除 Baz 对象将自动调用 Foo 和 Bar 类的析构函数,调用顺序与类在类定义中的顺序相反
    class Baz : public Foo, public Bar {
        // ...
    };
    
  • 使用具有多个父类的类对象与使用具有单个父类的类对象没什么不同。实际上,客户代码甚至不需要知道这个类有两个父类,需要关心的只是这个类支持的属性和行为

5.2 名称冲突和歧义基类

5.2.1 名称歧义
class Dog {
public:
    virtual void bark() {
        cout << "Woof!" << endl;
    }
    virtual void eat() {
        cout << "The dog ate." << endl;
    }
};

class Bird {
public:
    virtual void chirp() {
        cout << "Chirp!" << endl;
    }
    virtual void eat() {
        cout << "The bird ate." << endl;
    }
};

class DogBird : public Dog, public Bird {};

int main() {
    DogBird myConfuseAnimal;
    myConfuseAnimal.eat(); // 报错,存在歧义
    return 0;
}

在这里插入图片描述

  • 为了消除歧义,可使用 dynamic_cast() 显式地将对象向上转型 (本质上是向编译器隐藏多余的方法版本),也可以使用歧义消除语法。下面的代码显示了调用 eat() 方法的 Dog 版本的两种方案
    dynamic_cast<Dog&>(myConfuseAnimal).eat();
    myConfuseAnimal.Dog::eat();
    
  • 另一种防止歧义错误的方式是使用 using 语句显式指定,在 DogBird 类中应继承哪个版本的 eat() 方法
    class DogBird : public Dog, public Bird {
    public:
        using Dog::eat;
    };
    
5.2.2 歧义基类
  • 另一种引起歧义的情况是从同一个类继承两次。例如,如果出于某种原因 Bird 类从Dog 类继承,DogBird 类的代码将无法编译,因为 Dog 变成了歧义基类
    • 数据成员也可引起歧义。若 Dog 和 Bird 类具有同名数据成员,当访问这个成员时,就会发生歧义错误
    class Dog {};
    class Bird : public Dog {};
    class DogBird : public Bird, public Dog {}; // 歧义错误:同一个类继承两次
    

在这里插入图片描述

  • 多个父类本身也可能有共同的父类。例如,Bird 和 Dog 类可能都是 Animal 类的派生类。C++ 允许这种类型的类层次结构,尽管仍然存在着名称歧义。例如,如果 Animal 类有一个公有方法 sleep(),DogBird 对象无法调用这个方法,因为编译器不知道调用 Dog 类继承的版本还是 Bird 类继承的版本

在这里插入图片描述

  • 使用 “菱形” 类层次结构的最佳方法是将最顶部的类设置为抽象类,将所有方法都设置为纯虚方法。由于类只声明方法而不提供定义,在基类中没有方法可以调用,因此在这个层次上就没有歧义
  • 下例实现了菱形类层次结构,其中有一个每个派生类都必须定义的纯虚方法 eat()。DogBird 类仍须显式说明使用哪个父类的 eat() 方法,但是 Dog 和 Bird 类引起歧义的原因是它们具有相同的方法,而不是因为从同一个类继承
    class Animal {
    public:
        virtual void eat() = 0;
    };
    
    class Dog : public Animal {
    public:
        virtual void bark() {
            cout << "Woof!" << endl;
        }
        virtual void eat() override {
            cout << "The dog ate." << endl;
        }
    };
    
    class Bird : public Animal {
    public:
        virtual void chirp() {
            cout << "Chirp!" << endl;
        }
        virtual void eat() override {
            cout << "Thr bird ate." << endl;
        }
    };
    
    class DogBird : public Dog, public Bird {
    public:
        using Dog::eat;
    };
    

6 有趣而晦涩的继承问题

6.1 修改重写方法的特征

6.2 继承的构造函数

6.3 重写方法时的特殊情况

6.4 派生类中的复制构造函数和赋值运算符

6.5 运行时类型工具

6.6 非 public 继承

  • 父类是否可以是 private 或 proteted ?实际上可以这样做,尽管二者并不像 public 那样普遍。如果没有为父类指定任何访问说明符,就说明默认是类的 private 继承、结构的 public 继承
    • 将父类的关系声明为 protected,意味着在派生类中,基类所有的 public 方法和数据成员都成为受保护的
    • 与此类似,指定 private 继承意味着基类所有的 public、protected 方法和数据成员在派生类中都成为私有的

    非 public 继承很少见,建议慎用这一特性

6.7 虚基类

  • 本章前面学习了歧义父类,当多个基类拥有公共父类时,就会发生这种情况,如下图所示。建议的解决方案是:让共享的父类本身没有任何自有功能,这样就永远无法调用这个类的方法,因此也就不存在歧义
  • 如果希望被共享的父类拥有自己的功能,C++ 提供了另一种机制:如果被共享的基类是一个虚基类,就不存在歧义
    • 虚基类是在类层次结构中避免歧义的好办法
    class Animal {
    public:
        virtual void eat() = 0;
        virtual void sleep() {
            cout << "zzzzz...." << endl;
        }
    };
    // 将 Animal 作为虚基类继承
    class Dog : public virtual Animal {
    public:
        virtual void bark() {
            cout << "Woof!" << endl;
        }
        virtual void eat() override {
            cout << "The dog ate." << endl;
        }
    };
    // 将 Animal 作为虚基类继承
    class Bird : public virtual Animal {
    public:
        virtual void chirp() {
            cout << "Chirp!" << endl;
        }
        virtual void eat() override {
            cout << "Thr bird ate." << endl;
        }
    };
    // Animal 作为虚基类,DogBird 对象就只有 Animal 类的一个子对象
    class DogBird : public Dog, public Bird {
    public:
        virtual void eat() override {
            Dog::eat();
        };
    };
    
    int main() {
        DogBird myConfuseAnimal;
        myConfuseAnimal.sleep(); // 不会报错,因为使用了虚基类
        return 0;
    }
    

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/680148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WMS中Choreographer 配合 VSYNC 中断信号

WMS中Choreographer 配合 VSYNC 中断信号 1、了解SurfaceFlinger中VSYNC信号刷新2、Choreographer 舞蹈编导2.1 Choreographer初始化2.2 FrameHandler中处理任务2.3 FrameDisplayEventReceiver初始化3.4 简易流程图 3、ViewRootImpl中scheduleTraversals3.1 postCallback 通过n…

java——IO与NIO

文章目录 1. 传统IO模型字节流字符流 2. NIO模型 Java中的IO&#xff08;输入输出&#xff09;是用于在程序中读取和写入数据的一种机制。Java提供了两种不同的IO模型&#xff1a;传统的IO模型和NIO&#xff08;New IO&#xff09;模型。 1. 传统IO模型 在传统的IO模型中&…

WPF本地化/国际化,多语言切换

之前写过winformwinform使用本地化&#xff0c;中英文切换_winform 中英文切换_故里2130的博客-CSDN博客 基本的技术差不多&#xff0c;但是后来又发现了一个ResXManager工具&#xff0c;可以更好方便快捷的使用。 首先下载&#xff0c;网络不好的话&#xff0c;去官网下载&a…

01背包简介

01背包问题&#xff08;0/1 Knapsack problem&#xff09;是一个经典的动态规划问题&#xff0c;它描述了在给定容量限制的情况下&#xff0c;如何选择一组物品放入背包&#xff0c;以使得物品的总价值最大化。 问题描述&#xff1a; 假设有一个背包&#xff0c;其容量为C。现…

VulnHub项目:Fawkes

1、靶机地址 HarryPotter: Fawkes ~ VulnHub 该篇为哈利波特死亡圣器系列最终部&#xff0c;也是最难的一个靶机&#xff0c;难度真的是逐步提升&#xff01;&#xff01;&#xff01; 2、渗透过程 确认靶机IP&#xff0c;kali IP&#xff0c;探测靶机开放端口 详细的扫描…

ICLR 23 | 工业视觉小样本异常检测最新网络:Graphcore

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文链接&#xff1a;https://openreview.net/pdf?idxzmqxHdZAwO 论文代码&#xff1a;尚未开源 1.背景 随着人工智能中深度视觉检测技术的快速发展&#xff0c;检测工业产品表面的异常/缺陷受到了前所未有…

scratch lenet(11): C语言实现 squashing function

文章目录 1. 目的2. Sigmoidal Function2.1 S2 用到 Sigmoidal Function2.2 Sigmoidal Function 的定义 3. Squashing Function3.1 改用 Sigmoid Suahsing function 术语3.2 具体到 hyperlolic tangent 这一 squahsing function 4. Squahsing function 的实现References 1. 目的…

设计模式之观察者模式笔记

设计模式之观察者模式笔记 说明Observer(观察者)目录观察者模式示例类图抽象主题角色类抽象观察者类具体主题角色类具体的观察者角色类测试类 说明 记录下学习设计模式-观察者模式的写法。JDK使用版本为1.8版本。 Observer(观察者) 意图:定义对象间的一种一对多的依赖关系&a…

Gradle构建系统macOS安装与使用

1.打开gradle.org并点击安装 2.先决条件 ,确认安装JDK1.8或者更高版本已安装 在终端输入brew install gradle进行安装 安装成功如下: 查看安装版本号gradle -v 使用gradle 1.创建目录demo并进入该目录 mkdir demo cd demo 2.gradle init 使用Gradle开始构建 输入2开始构建应…

DevOps系列文章之 docker插件实现多实例部署(IDEA插件)

1. Docker的安装以及开启远程访问 1.1 安装 # 检查虚拟机内核版本&#xff0c;必须是3.10及以上 uname -r # 安装docker yum install docker # 输入y确认安装 # 启动docker systemctl start docker # 查看docker版本 docker -v # 开机启动docker systemctl enable docker # 停…

Golang学习日志 ━━ gin-vue-admin换机重新配置的记录,很愚蠢,很傻瓜,很机械...自己使用

最近一直在弄AI&#xff0c;没时间搞gva&#xff0c;所以有点忘记了&#xff0c;代码升级管它呢&#xff0c;全部重来一遍~ 一、备份保存 根据经验和个人喜好&#xff0c;我特别不喜欢在框架下把一个应用分散在module、api、service等等目录下&#xff0c;这种目录分配方案将把…

图上作业法

目录 交通示意图的表示方法 图上作业法 &#xff08;1&#xff09;对流 &#xff08;2&#xff09;迂回 物资调运问题的图上作业法 交通路线不成圈 交通路线成圈 交通示意图的表示方法 交通示意图是用来表明收发点的大致位置、收发量、交通路线长度的图形。 图形表示…

java mail发送、接收邮件

java mail接收邮件 1、引入java mail依赖 <dependency><groupId>org.eclipse.angus</groupId><artifactId>angus-mail</artifactId><version>2.0.2</version> </dependency>2、编写代码 注意&#xff1a;下述代码中的服务器…

从BNO055传感器获取IMU数据-2

在前面的文章 从BNO055传感器获取IMU数据-1 中介绍了BNO055传感器&#xff0c;今天继续讲解应用示例。 传感器与Arduino接口 我从某宝购买了固定在带有支持组件的开发板上的 BNO055 传感器。从 Digi-Key 或贸泽购买 BNO055 并将其焊接到 7.54.4mm 28 引脚 LGA 至 DIP 转换器上…

Groovy基础

Groovy基础 学Groovy有什么用&#xff1f;&#xff1f;&#xff1f;一、Groovy简介二、Mac安装Groovy1、使用homebrew安装或官网下载2、配置环境变量3、重新加载环境变量 二、Groov基本语法三、更多特性和扩展四、Groovy简单使用1、Groovy中的字符串及三大语句结构2、Groovy类与…

1带你入门MATLAB图像处理图像类型转换(附matlab程序)

1.简述 学习目标&#xff1a; 图像类型的转换 常用图像格式 图像格式&#xff1a;是存储图像采用的文件格式。不同的操作系统、不同的图像处理软件&#xff0c;所支持的图像格式都有可能不同。 在实际应用中经常会遇到的图像格式有&#xff1a;BMP、GIF、TIFF、PCX、JPEG、P…

FFmpeg5.0源码阅读——avformat_open_input

摘要&#xff1a;本文主要描述了FFmpeg中用于打开文件接口avformat_open_input的具体调用流程&#xff0c;详细描述了该接口被调用时所作的具体工作。   关键字&#xff1a;ffmpeg、avformat_open_input   注意&#xff1a;读者需要了解FFmpeg的基本使用流程&#xff0c;以…

力扣动态规划专题(五)子序列问题 不连续子序列与连续子序列 步骤及C++实现

文章目录 300.最长递增子序列674.最长连续递增子序列动态规划贪心算法 718. 最长重复子数组二维dp数组一维dp数组 1143.最长公共子序列1035.不相交的线53. 最大子序和动态规划贪心算法 300.最长递增子序列 步骤 确定dp数组以及下标的含义 dp[i]&#xff1a;i之前&#xff08;包…

【数据结构】单链表 创建 插入 删除 查找 完整代码

3.1 单链表 3.1.1 定义 注&#xff1a; 元素离散的分布在存储空间中&#xff0c;所以单链表是非随机存取的存储结构。 即不能直接找到表中某个特定的结点&#xff0c;需要从表头开始遍历&#xff0c;依次查找。 定义的代码 typedef struct LNode {ElemType data;//每个节点存放…