"C++ Advanced Programming" Reading Notes (10: Demystifying Inheritance Technology)

1. References

2. It is recommended to read the book "21 Days to Learn C++" to get started. The link of the notes is as follows

1. Building classes using inheritance

1.1 Extension class

  • When writing a class definition in C++, you tell the compiler that the class inherits (or extends) an existing class. In this way, the class will automatically contain the data members and methods of the original class
    • The original class is called parent class, base class or super class
    • Extending an existing class allows the class (now called a derived class or subclass ) to describe only that part of the content that is different from the parent class
  • In C++, in order to extend a class, you can specify the class to be extended when defining the class, here the classes named Base and Derived are used
    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 Customers' Views on Inheritance
  • When calling a method, the code using the derived class does not need to know which class in the inheritance chain defines the method
    // 调用了 Derived 对象的两个方法,而其中一个方法是在 Base 类中定义的
    Derived myDerived;
    myDerived.someMethod();
    myDerived.someOtherMethod();
    
    Base myBase;
    myBase.someOtherMethod(); // 错误,继承是单向的
    
  • The way inheritance works is one-way , the Derived class has a clear relationship to the Base class, but the Base class doesn't know anything about the Derived class . This means that objects of type Base do not support public methods and data members of class Derived, because class Base is not a class of Derived

insert image description here

  • A pointer or reference to an object can point to an object of the declaring class, or to an object of any of its derived classes. A pointer to a Base object can point to a Derived object, and the same is true for references . Clients still only have access to the methods and data members of the Base class, but through this mechanism, any code that manipulates the Base object can manipulate the Derived object
    Base* base = new Derived();
    // 尽管对象是 Derived 类型,但编译器只是将它看成 Base 类型,而 Base 类型没有定义 someOtherMethod() 方法
    base->someOtherMethod(); // 错误
    
1.1.2 Analysis of inheritance from the perspective of derived classes
  • Derived classes can access public and protected methods and data members declared in the base class, but cannot access private methods and data members declared in the base class
    • It is recommended that all data members be declared private by default
      • If you want any code to be able to access these data members, provide public getters and setters
      • Provide protected getters and setters if you only want derived classes to access them
    • Reasons for setting data members as private by default
      • This provides the highest level of encapsulation, meaning that the representation of the data can be changed while the public or protected interface remains the same. Without directly accessing data members, it is also convenient to add checks for input data in public or protected setters
1.1.3 Disable inheritance
  • C++ allows a class to be marked final, which means that inheriting this class will cause a compile error
    class Base final {
          
          
        //...
    };
    
    class Derived : public Base {
          
           // 错误,因为 Base 声明为 final 表示禁用继承
        // ...
    };
    

1.2 Rewrite method

  • In many cases, it may be necessary to replace or override a method to modify the behavior of a class
1.2.1 Set all methods to virtual, just in case
  • In C++, overriding (override) methods is a bit awkward, only methods declared virtual in the base class can be correctly overridden by derived classes
    class Base {
          
          
    public:
        virtual void someMethod();
    protected:
        int mProtectedInt;
    private:
        int mPrivateInt;
    };
    
  • Even though the Derived class is unlikely to be extended, it's still a good idea to make the methods of this class virtual, just in case
    class Derived : public Base {
          
          
    public:
        virtual void someOtherMethod();
    };
    

As a rule of thumb, to avoid problems caused by omitting the virtual keyword, make all methods virtual (including destructors, but not constructors). Note that the destructor generated by the compiler is not virtual

1.2.2 Syntax for overriding methods
  • In order to override a method, you need to redeclare the method in the definition of the derived class and provide the new definition in the implementation file of the derived class
    // 基类中 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;
    };
    

    Once a method or destructor is marked virtual, they remain virtual in all derived classes, even if the virtual keyword is removed in derived classes

1.2.3 Customers' views on overriding methods
  • The following code works as before, calling the Base version of someMethod()
    Base myBase;
    myBase.someMethod();
    
  • If you declare a Derived class object, the derived class version of someMethod() will be called automatically
    Derived myDerived;
    myDerived.someMethod();
    
  • If a reference to a Base object actually refers to a Derived object, calling someMethod() will actually call the derived class version, as shown below
    Derived myDerived;
    Base& ref = myDerived;
    ref.someMethod(); // 调用 Derived's version someMethod()
    
  • Even if the base class reference or pointer knows that this is actually a derived class, it cannot access derived class methods or members that are not defined in the base class . The following code does not compile because the Base reference has no someOtherMethod() method
    Derived myDerived;
    Base& ref = myDerived;
    myDerived.someOtherMethod(); // 可行
    ref.someOtherMethod(); // 错误
    
  • Objects that are not pointers or references cannot correctly handle derived class trait information . Derived objects can be converted to Base objects, or Derived objects can be assigned to Base objects, because Derived objects are also Base objects. However, at this point the object will lose all information about the derived class
    Derived myDerived;
    Base assignedObject = myDerived;
    assignedObject.someMethod(); // 调用 Base's version someMethod()
    
  • When a base class pointer or reference points to a derived class object, the derived class retains its overridden methods. But when a derived class object is converted to a base class object through type conversion, its unique characteristics will be lost. The loss of overridden methods and derived class data is called truncation
    • Think of the Base object as a box that occupies a block of memory. The Derived object is the slightly larger box, because it has everything the Base object has, plus a little more content. For references or pointers to Derived objects, the box hasn't changed: it's just accessed in a new way. However, if you convert the Derived object to a Base object, you will throw away all the "unique features" of the Derived class in order to fit into the smaller box
1.2.4 override keyword
  • The following Derived does not override the someMethod() of the Base class, but creates a new virtual method , which can be avoided with the override keyword
    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 The truth about virtual
hide rather than override
  • Since this method is not virtual, it is not actually overridden. Instead, the Derived class creates a new method with the name go(), which has nothing to do with the go() method of the Base class.
    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"
    
How to implement virtual
  • Static binding (early binding) : When C++ compiles a class, it creates a binary object that contains all the methods in the class. In the non-virtual case, the code that hands control to the correct method is hard-coded, and the method is invoked according to the compile-time type
  • Dynamic binding (late binding) : If a method is declared virtual, the correct implementation is invoked using a specific memory area called a virtual table (vtable). Each class with one or more virtual methods has a virtual table, and each object of this class contains a pointer to the virtual table, and this virtual table contains a pointer to the implementation of the virtual method . In this way, when a method is called with an object, the pointer also enters the virtual table, and then the correct version of the method is executed according to the actual object type
    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;
    
  • The following figure shows a high-level view of two instance vtables
    • The myBase object contains a pointer to the virtual table. The virtual table has two items, one is func1() and the other is func2(). These two items point to the implementation of Base::func1() and Base::func2()
    • myDerived also contains a pointer to a vtable that also contains two entries, one for func1() and the other for func2(). The func1() item of myDerived virtual table points to Base::func1(), because the Derived class does not override func1(); but the func2() item of myDerived virtual table points to Derived::func2()
    • Note that neither virtual table contains an entry for the nonVirtualFunc() method, because the method is not set to virtual
      insert image description here
The need for virtual destructors
  • The destructor should be declared virtual , if the destructor is not declared virtual, it is easy to not free the memory when destroying the object. The only exception to not declaring a destructor virtual is when the class is marked final (inheritance disabled)
  • If you do nothing in the destructor and just want to make it virtual, you can explicitly set "= default"
    class Base {
          
          
    public:
        virtual ~Base() = default;
    };
    
1.2.6 Disable rewriting
  • C++ allows methods to be marked final, which means that the method cannot be overridden in derived classes
    class Base {
          
          
    public:
        virtual ~Base() = default;
        virtual void someMethod() final;
    };
    

2. Use inheritance to reuse code

2.1 WeatherPrediction class

  • Assume that you want to write a simple weather forecast program that gives Fahrenheit and Celsius temperatures at the same time. The definition of the WeatherPrediction class is as follows
    // 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 Adding and replacing functionality in derived classes

  • Define a new class MyWeatherPrediction, which inherits from the WeatherPrediction class
  • The first step towards supporting Celsius is to add new methods that allow setting the current temperature in Celsius and thus getting tomorrow's weather forecast in Celsius. Also include a private helper method for converting between Celsius and Fahrenheit . These methods can be static methods as they are the same for all instances of the class
    // 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. Use the parent class

3.1 Parent class constructor

  • Objects are not created suddenly. When creating an object, the parent class and the objects contained in it must be created at the same time. C++ defines the following creation order
    • If a class has a base class, execute the default constructor of the base class. Unless a base class constructor is called in the initialization list, this constructor is called instead of the default constructor at this time
    • Non-static data members of a class are created in the order they are declared
    • Execute the constructor of the class
    // 通常不建议在类定义中直接实现方法,此处为了方便演示
    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'
    
  • Passing constructor parameters from a derived class to a base class is normal, but data members cannot be passed. If you do this, the code compiles, but the data members are not initialized until after the base class constructor is called . If a data member is passed as an argument to a superclass constructor, the data member is not initialized

The behavior of the virtual method is different in the constructor. If the derived class overrides the virtual method in the base class and calls the virtual method from the base class constructor, the base class implementation of the virtual method will be called instead of the derived class. rewritten version

3.2 The destructor of the parent class

  • Since the destructor has no parameters, the superclass's destructor is always called automatically. Destructors are called in the opposite order of constructors
    • call the destructor of the class
    • Data members of a class are destroyed in reverse order of creation
    • If there is a parent class, call the destructor of the parent class
    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'
    
  • Declare all destructors as virtual. The default destructor generated by the compiler is not virtual, so you should define your own (or explicitly set as the default) virtual destructor, at least in the parent class
  • As with constructors, virtual methods behave differently when called in a destructor. If a derived class overrides a virtual method in a base class and calls the method in the destructor of the base class, the base class implementation of the method will be executed instead of the overridden version of the derived class

3.3 Using parent class methods

  • When a method is overridden in a derived class, it effectively replaces the original method. However, the parent class version of the method still exists and the methods can still be used
  • In C++, calling the superclass version of the current method is a common operation. If there is a chain of derived classes, each derived class may want to perform operations already defined in the base class, while adding its own additional functionality, example: a class hierarchy for a book type
    • The Book base class has two virtual methods: getDescription() and getHeight()
    • All derived classes override getDescription(), only the Romance class overrides getHeight() by calling getHeight() of the parent class (Paperback) and then dividing the result by 2
    • The Paperback class does not override getHeight(), so C++ goes up the class hierarchy looking for a class that implements getHeight()
    • In this case, Paperback::getHeight() will resolve to 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;
    }
    

insert image description here

3.4 Upward transformation and downward transformation

  • Using a derived class to assign a pointer or reference to the base class will not cause truncation. This is the correct way to use the derived class through the base class, also known as upward transformation . This is also the reason for methods and functions to use class references instead of class objects directly. When using references, derived classes are not truncated when passed
    Base myBase = myDerived; // 截断
    Base& myBase = myDerived; // 不会截断
    
  • Converting a base class to its derived class is also called downcasting . Downcasting should only be used when necessary, and dynamic_cast() should be used to use the built-in type information of the object and reject meaningless type conversions
    • This built-in information usually resides in virtual tables, and the dynamic_cast operator can only be used for conversions between classes with virtual functions , since it requires checking the actual type of the object at runtime. If a problem occurs during type conversion, such as trying to convert a pointer to a non-polymorphic type to a pointer to a derived class type, the program may exhibit undefined behavior or crash
      • If a dynamic_cast() fails on a pointer, the value of the pointer is nullptr instead of pointing to some meaningless data
      • A std::bad_cast exception will be thrown if dynamic_cast() on an object reference fails
    // lessPresumptuous() 实际只能用于 Derived 对象,只应接受 Derived 指针
    void lessPresumptuous(Base* base) {
          
          
        // 使用 dynamic_cast 运算符将指向 Base 类型对象的指针进行类型转换
        // 得到一个指向 Derived 类型对象的指针 myDerived
        Derived* myDerived = dynamic_cast<Derived*>(base);
        if (myDerived != nullptr) {
          
          
            // ...
        }
    }
    

4. Inheritance and polymorphism

4.1 Designing Polymorphic Spreadsheet Cells

  • The definition of the SpreadsheetCell class in classes and objects, where cells can be double, string or other types, polymorphic spreadsheet cells can be designed to achieve this purpose
  • The diagram below shows how the SpreadsheetCell hierarchy is polymorphic. Since both DoubleSpreadsheetCell and StringSpreadsheetCell inherit from the same parent class SpreadsheetCell , they are interchangeable from the perspective of other code, which in practice means
    • Both derived classes support the same interface (method set) defined by the base class
    • Code that uses a SpreadsheetCell object can call any method in the interface without knowing whether the cell is a StringSpreadsheetCell or a DoubleSpreadsheetCell
    • Due to the special ability of virtual methods, the correct instance of each method in the interface will be called according to the class to which the object belongs
    • Other data structures can contain a set of multi-type cells by referencing the parent type

insert image description here

4.2 SpreadsheetCell base class

4.2.1 First attempt
  • The SpreadsheetCell base class is responsible for defining the behavior supported by all derived classes . All cells in this example need to have their values ​​set to strings. Also, all cells need to return the current value as a string. The methods are declared in the base class definition, along with a virtual destructor explicitly set to default, but no data members
    // 这个父类声明了派生类支持的行为,但是并不定义这些行为的实现
    class SpreadsheetCell {
          
          
    public:
        virtual ~SpreadsheetCell() = default;
        virtual void set(std::string_view inString);
        virtual std::string getString() const;
    };
    
4.2.2 Pure virtual methods and abstract base classes
  • A pure virtual method explicitly states in the class definition that the method does not need to be defined . If a method is set as a pure virtual method, it tells the compiler that the definition of this method does not exist in the current class. A class with at least one pure virtual method is called an abstract class because it has no instances . The compiler enforces the fact that if a class contains one or more pure virtual methods, objects of this type cannot be constructed
    • A pure virtual method is specified using a special syntax: the method declaration is followed by = 0
    • An abstract class provides a way to prevent other code from instantiating objects directly, while its derived classes can instantiate objects
    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 Independent derived classes

  • Writing the StringSpreadsheetCell and DoubleSpreadsheetCell classes only needs to implement the functions defined in the parent class
    • Want to implement and use string cells and double value cells, so cells should not be abstract: all pure virtual methods inherited from parent classes must be implemented
    • If the derived class does not implement all the pure virtual methods inherited from the parent class, then the derived class is also abstract, and objects of the derived class cannot be instantiated
4.3.1 StringSpreadsheetCell class definition and implementation
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 definition and implementation
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 Using polymorphism

  • SpreadsheetCell is an abstract class, so objects of this type cannot be created. However, a pointer or reference to SpreadsheetCell can still be used since it actually points to one of the derived classes
  • Different objects accomplish this in different ways when the getString() method is called. StringSpreadsheetCell returns the value it stores, or an empty string. DoubleSpreadsheetCell performs the conversion first if it contains a value; otherwise returns an empty string
    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. Multiple inheritance

5.1 Inheriting from multiple classes

  • From a syntactic point of view, defining a class with multiple parents is straightforward, and since multiple parents are listed, a Baz object has the following properties
    • The Baz object supports the public methods of the Foo and Bar classes, and contains the data members of these two classes
    • Methods of class Baz have access to protected data members and methods of classes Foo and Bar
    • Baz objects can be upcast to Foo or Bar objects
    • Creating a new Baz object will automatically call the default constructors of the Foo and Bar classes and in the order of the classes listed in the class definition
    • Deleting the Baz object will automatically call the destructors of the Foo and Bar classes in the reverse order of the classes in the class definition
    class Baz : public Foo, public Bar {
          
          
        // ...
    };
    
  • Using a class object with multiple parents is no different than using a class object with a single parent. In fact, client code doesn't even need to know that this class has two parent classes, all it needs to care about is the attributes and behaviors supported by this class

5.2 Name conflicts and ambiguous base classes

5.2.1 Name Ambiguity
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;
}

insert image description here

  • To disambiguate, either explicitly upcast the object using dynamic_cast() (essentially hiding the redundant method version from the compiler), or use the disambiguation syntax. The code below shows two scenarios for calling the Dog version of the eat() method
    dynamic_cast<Dog&>(myConfuseAnimal).eat();
    myConfuseAnimal.Dog::eat();
    
  • Another way to prevent ambiguity errors is to use a using statement to explicitly specify which version of the eat() method should be inherited in the DogBird class
    class DogBird : public Dog, public Bird {
          
          
    public:
        using Dog::eat;
    };
    
5.2.2 Ambiguous base classes
  • Another situation that causes ambiguity is inheriting from the same class twice . For example, if for some reason the Bird class inherits from the Dog class, the code for the DogBird class will not compile because Dog becomes an ambiguous base class
    • Data members can also cause ambiguity. If the Dog and Bird classes have data members with the same name, an ambiguity error will occur when accessing this member
    class Dog {
          
          };
    class Bird : public Dog {
          
          };
    class DogBird : public Bird, public Dog {
          
          }; // 歧义错误:同一个类继承两次
    

insert image description here

  • Multiple parent classes may themselves have a common parent class. For example, the Bird and Dog classes might both be derived classes from the Animal class. C++ allows this type of class hierarchy, although there is still name ambiguity. For example, if the Animal class has a public method sleep(), the DogBird object cannot call this method because the compiler does not know whether to call the version inherited by the Dog class or the version inherited by the Bird class

insert image description here

  • The best way to use a "diamond" class hierarchy is to make the topmost class abstract and all methods pure virtual . Since the class only declares methods and does not provide definitions, there is no method in the base class to call, so there is no ambiguity at this level
  • The following example implements a diamond class hierarchy with a pure virtual method eat() that every derived class must define. The DogBird class still has to explicitly say which parent class's eat() method to use , but the Dog and Bird classes are ambiguous because they have the same method, not because they inherit from the same class
    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 Interesting and Obscure Inheritance Problems

6.1 Modify the characteristics of the overridden method

6.2 Inherited constructors

6.3 Special cases when overriding methods

6.4 Copy constructor and assignment operator in derived classes

6.5 Runtime type tools

6.6 Non-public inheritance

  • Can the parent class be private or protected? It's actually possible to do this, although the two are not as common as public. If no access specifier is specified for the parent class, it means that the default is the private inheritance of the class and the public inheritance of the structure
    • Declaring the relationship of the parent class as protected means that in the derived class, all public methods and data members of the base class become protected
    • Similarly, specifying private inheritance means that all public and protected methods and data members of the base class become private in the derived class

    Non-public inheritance is rare, it is recommended to use this feature with caution

6.7 Virtual base classes

  • Earlier in this chapter, we learned about ambiguous parent classes, which happens when multiple base classes have a common parent class, as shown in the following figure. The suggested solution is: make the shared parent class not have any own functionality itself, so that the method of this class can never be called, so there is no ambiguity
  • If you want the shared parent class to have its own functions, C++ provides another mechanism: if the shared base class is a virtual base class, there is no ambiguity
    • Virtual base classes are a great way to avoid ambiguity in class hierarchies
    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;
    }
    

insert image description here

Guess you like

Origin blog.csdn.net/qq_42994487/article/details/131364833