[C + +] Chapter 11: Inheritance

[C + +] Chapter 11: Inheritance

1, Concept of inheritance

1. First acquaintance of inheritance

When writing a function with the same name with the same function, C + + syntax creates syntax such as "function overload", "function template" in order to simplify the code. However, if there are many similarities and differences between the two classes, the C + + syntax also has targeted syntax to solve this problem in order to simplify the code, which is today's * * inheritance * *.

Inheritance mechanism is the most important means for object-oriented programming to make code reusable. It allows programmers to extend and add functions on the basis of maintaining the characteristics of the original class, so as to produce new classes, called derived classes. Inheritance presents the hierarchical structure of object-oriented programming and embodies the cognitive process from simple to complex.

Let's come down and see what inheritance looks like?

#include <iostream>
#include <string> 

using namespace std;

class Student // Base class 
{
public:
    void print() {
        cout << "I am a student !" << endl;
    }
protected:
    string _name;
    int _age; 
};

class Junior : public Student // Public inherited Student class
{
protected:
    int _junId;
};

class Senior : public Student // Public inherited Student class
{
protected:
    int _senId; 
}; 

int main()
{
    // Derived classes can use idiom functions of the base class
    // And has member variables of the base class
    Junior junior_stu;
    junior_stu.print(); // Output I am a student!

    Senior senior_stu;
    senior_stu.print(); // Output I am a student!
    return 0;
}

Explanation:

After inheritance, the members in the parent class Student class (including member functions and member variables) will become part of the child class. This shows that using inheritance can reuse code and simplify code.

As can be seen from the main function above, the subclass can use the member function of the parent class.

The following figure shows that the subclass contains the member variables of the parent class.

2. How inheritance is defined

2.1. Define inheritance format

2.2. Comparison of inheritance methods and access qualifiers

2.3. Change of access mode of member variables in derived classes after inheritance

Class members \: inheritance methodspublic inheritanceprotected inheritanceprivate inheritance
public member of base classpublic member (in a derived class)protected member (in a derived class)private member (in a derived class)
protected member of base classprotected member (in a derived class)protected member (in a derived class)private member (in a derived class)
private member of base classInvisible (in derived classes)Invisible (in derived classes)Invisible (in derived classes)

3. Summary

  • private members in the base class are not visible after inheritance. What is invisible here does not mean that there is no member variable of the base class in the derived class, but syntactically restricts the access of the member after inheritance.

  • A table summarizing the changes in access methods can be found

    • The * * private member * * in the base class is invisible after inheritance
    • Private members and public members in the base class. After inheritance, the inheritance method and base class members are limited to the smaller one (public > protected > private). For example, after protected inheritance, the public member in the base class becomes a protected member because protected < public. (it can be so convenient for memory)
  • If you want a member variable in a class to be accessible in a derived class rather than in an object outside the class, you can use protected members, which is the value of protected members.

  •   int main()
      {
          // Junior class public inherits Student class
          Junior junior_stu;
          junior_stu.print(); // print() is public in the base class, so it can be accessed outside the class
          junior_stu._age; // _ age is protected in the base class, so it can be accessed in the class, not outside the class
      	return 0;
      }
    
  • In general, you need to explicitly write out the inheritance method of the class, but if you do not write the inheritance method, the class defined by class defaults to private inheritance, while the class defined by struct defaults to public inheritance.

    class Junior : Student // Private by default. This is not recommended
    {
    protected:
        int _junId;
    };
    
    struct Senior :  Student // Default public. This is not recommended
    {
    protected:
        int _senId; 
    }; 
    
  • In general, the inheritance method is public inheritance.

2, Assignment transformation (slicing) of base and derived class objects

Understand the concept of inheritance. Let's take a look at whether the base class object and the derived class object can assign values to each other.

Concepts needing attention:

1. The derived class object can be assigned to the base class object, but the base class object cannot be assigned to the derived class object. This phenomenon is like cutting off the part of the base class object in the derived class object, so it is called "slicing".

#include <iostream>
#include <string> 

using namespace std;

class Student // Base class 
{
protected:
    string _name;
    int _age; 
};

class Junior : public Student // Public inherited Student class
{
protected:
    int _junId;
};

int main()
{
    Junior j;
    Student s = j; // The derived class object is assigned to the base class
    // Junior tmpJu = s; // A base class cannot be assigned to a derived class 
    return 0;
}

2. The address of the derived class can be assigned to the pointer of the base class, and the object of the derived class can also be assigned to the reference of the base class. The base class pointer cannot directly point to the pointer of the derived class object, but the pointer of the base class can be cast into the pointer of the derived class object and then assigned to the derived class pointer. The base class object cannot reference the object of the derived class

#include <iostream>
#include <string> 

using namespace std;

class Student // Base class 
{
public:
    void print() {
        cout << "I am a student !" << endl;
    }
protected:
    string _name;
    int _age; 
};

class Junior : public Student // Public inherited Student class
{
protected:
    int _junId;
};

int main()
{
    Junior j;
    Student* ps = &j; // A base class object pointer can point to a derived class object
    Student& rs = j;  // Base class object references can reference derived class objects
    Junior* tmpJu = (Junior*)ps; // After the strong rotation of the base class pointer, it can be assigned to the derived class pointer
    return 0;
}

3, Scope in inheritance

Concepts needing attention:

1. In the inheritance system, both base and derived classes have independent scopes.

2. There are functions with the same name in the subclass and parent class. The subclass members will block the access of the members with the same name in the parent class. This phenomenon is called "hiding", also known as "redirection".

Notes on hiding:

  • Hiding and function overloading should be distinguished. Function overloading is that two functions with the same name have different function parameter lists under the same scope, so the two functions with the same name constitute "overloading". Hiding is two functions with the same name (there is no requirement for the function parameter list). Under different scopes, these two functions with the same name constitute "hiding".
  • Two member variables with the same name will also form a hidden. You can use:: to directly and explicitly access member variables. (generally, member variables with the same name should not appear, otherwise inheritance is of little significance).
#include <iostream>
#include <string> 

using namespace std;

class Student // Base class 
{
public:
    void print() {
        cout << "I am a student !" << endl;
    }
    string _name;
    int _age = 1; 
};

class Junior : public Student // Public inherited Student class
{
public:
    void print() {
        cout << "I am a Junior !" << endl;
    }
    int _junId;
    int _age = 2;
};

int main()
{
    Junior j;
    j.print(); // Hide the print() function of the base class and output I am a junior!
    cout << j._age << endl; // Hidden base class_ age, output 2

    j.Student::print(); // Display call and output I am a student!
    cout << j.Student::_age << endl; // Display call, output 1
    return 0;   
}

4, Default member functions in derived classes

After understanding the use of derived class objects, let's go deeper to understand the internal default member functions of derived class objects.

Each class has six default member functions, that is, the class will automatically generate six default member functions when creating objects.

1. Constructor: the constructor of the derived class must call the constructor of the base class to complete the part of the base class member variable in the derived class. If there is no default constructor in the base class to call, it must be explicitly called in the initialization list of the constructor of the derived class.

2. Copy constructor: similar to the constructor, the copy constructor of the derived class must call the copy constructor of the base class to complete the assignment of the part of the base class object in the derived class.

3. Overload of assignment operator: similar to constructor, operator = of base class must be called to complete assignment of base class object in derived class object.

4. The design of destructor is very special.

  • Under the inheritance relationship, the destructors of the child class and the parent class constitute concealment. This is because at the bottom, these two functions with different names will be converted into a function called destroy (), which will constitute concealment. Therefore, if you want to call the destructor of the parent class, you need to use:: to explicitly call the destructor of the parent class.
  • However, in the decomposition order of a class, the base class is constructed first and the derived class is constructed. According to the concept of first in first out, the subclass is constructed first and then the base class is constructed. Therefore, by default, after the subclass calls the destructor, it will automatically call the destructor of the parent class.

	#include <iostream>
#include <string> 

using namespace std;

class Student
{
public:
    Student(string name, int age):
        _name(name), _age(age) {}

    Student(const Student& s):
        _name(s._name), _age(s._age) {}

    Student& operator= (const Student& s) {
        if (this != &s) {
            _name = s._name;
            _age = s._age;
        }
        return *this;
    }

    ~Student() {}

protected:
    string _name;
    int _age;
};

class Junior : public Student
{
public:
    Junior(string name, int age, int id):
        Student(name, age),
        _id(id) {}

    Junior(const Junior& j):
        Student(j), // The derived class object is assigned to the base class object for slicing
        _id(j._id) {}

    Junior& operator= (const Junior& j) {
        if (this != &j) {
            // Here, we need to assign values to the base class in the derived class, so we need to explicitly call the operator of the parent class=
            (*this).Student::operator=(j); 
            _id = j._id;
        }
        return *this;
    }

    ~Junior() {} // After calling the destructor of the derived class, the destructor of the base class is automatically called
    
    void print() {
        cout << _name << ' ' << _age << ' ' << _id << endl;
    }
protected:
    int _id;
};

5, Friends in inheritance

Friend relationships cannot be inherited, that is, friend functions in the parent class cannot access private or protected members in the child class.

class Junior;
class Student
{
public:
    friend void Display(const Student &s, const Junior &j);

protected:
    string _name; // full name
};
class Junior : public Student
{
public:
    // friend void Display(const Student &s, const Junior &j);
protected:
    int _stuNum; // Student number
};

void Display(const Student& s, const Junior& j)
{
    cout << s._name << endl;
    cout << j._stuNum << endl;
}

int  main()
{
    Student s;
    Junior j;
    // If only the parent class has friend functions, members in Junior class objects cannot be accessed
    Display(s, j); // Cannot call
    return 0;
}

6, Static members in inheritance

If static static members are defined in the base class, there is only one member variable in the whole inheritance system, that is, no matter how many subclasses are derived, there is only one instance of static members.

class Student
{
public:
    Student() {
        ++ count ;
    }
    static int count;
};
 
int Student::count = 0;

class Junior : public Student
{
protected:
    int _id;
};

int main()
{
    Student s1;
    Student s2;
    Junior j1;
    cout << "Created: " << Student::count  << " Objects"<< endl; // Output: 3 objects created
    return 0;
}

7, Inheritance mode

According to the number of parent classes, inheritance is divided into single inheritance and multi inheritance.

1. Single inheritance

A subclass has only one direct parent, and this inheritance is called "single inheritance".

2. Multiple inheritance

A subclass with two or more direct parents is called multi inheritance.

2.1. The "scourge" of multi processes

There is a special case in multiple inheritance called diamond inheritance.

class Student
{
public:
    string _name; // full name
};

class Junior : public Student
{
protected:
    int _num; // Student number
};

class Senior : public Student
{
protected:
    int _id; // number
};

class Graduate : public Junior, public Senior
{
protected:
    string course; // curriculum  
};

Problem of diamond inheritance: from the construction of object member model, we can see that diamond inheritance has the problems of "data redundancy" and "ambiguity".

Graduate objects inherit from Junior and Senior classes, and Junior and Senior both inherit student classes, so they are available in Junior and Senior classes_ name member, so there will be two members of Student object in the graduate object.

Because there are two members of the Student object, you are using the_ When name is a member, there will be ambiguity. Do you want to use a member in Junior or a member in Senior? So this is the problem of ambiguity.

This problem can actually be solved. We can use the calling member displayed by the domain scope:

Graduate person;
// Use:: displays the members of the object
person.Junior::_name = "Zhang San";
person.Senior::_name = "Zhang San";

However, there is no way to solve the data redundancy caused by object members that store two copies.

However, virtual inheritance is used in C + + syntax to solve this problem.

2.2. Solution to diamond inheritance - virtual inheritance

Virtual inheritance can solve the ambiguity of diamond inheritance and data redundancy. As for the inheritance relationship above, the problem can be solved by using virtual inheritance when Junior and Senior inherit students.

class Student
{
public:
    string _name; // full name
};

class Junior : virtual public Student // Virtual inheritance
{
protected:
    int _num; // Student number
};

class Senior : virtual public Student // Virtual inheritance
{
protected:
    int _id; // number
};

class Graduate : public Junior, public Senior
{
protected:
    string course; // curriculum  
};

int main()
{
    Graduate person;
    person._name = "Zhang San";
    // Solved the problem of ambiguity
    cout << person._name << endl;         // Print Zhang San
    // The problem of data redundancy is solved
    cout << &person._name << endl;        // 0x61fdf0
    cout << &person.Senior::_name << endl;// 0x61fdf0
    cout << &person.Junior::_name << endl;// 0x61fdf0
    return 0;
}

After Junior and Senior inherit from virtual, you can directly modify the attributes in the Graduate object without using::_ The name member operates, which has solved the ambiguity problem. According to the printed addresses of the three members, it can be inferred that there is only one copy in the Graduate object_ Name member, which also solves the problem of data redundancy.

Note: when using virtual inheritance, Junior and Senior virtual inherit Student, not Graduate virtual inherit Junior and Senior.

2.3. Principle of virtual inheritance

Let's use a simple example to illustrate the principle of virtual inheritance.

First, let's take a look at a small example without virtual inheritance:

The code is as follows:

#include <iostream>
#include <string>

using namespace std;

class A {
public:
	int _a;
};

class B : public A {
public:
	int _b;
};

class C : public A {
public:
	int _c;
};

class D : public B, public C {
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

This is also a typical diamond inheritance model:

The memory model is as follows:

Now let's take a look at what happens in memory_ a,_ b,_ c,_ How is d stored? Is there any rule.

We found that in the debug memory window of the compiler, we can see that there are really two copies inside the d object_ a. And they are all in B/C objects.

Let's take a look at the memory layout of objects after the virtual process.

The code is as follows:

#include <iostream>
#include <string>

using namespace std;

class A {
public:
	int _a;
};

class B : virtual public A { // Virtual inheritance
public:
	int _b;
};

class C : virtual public A { // Virtual inheritance
public:
	int _c;
};

class D : public B, public C {
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

Inherited model:

It is actually stored in memory as follows:

Explain the above figure:

We can find the difference between B object members and C object members inherited from the original D object_ A has become a strange address, and_ A member stores a copy at the end of D object. This is why virtual can solve the ambiguity problem and data redundancy problem. And the original_ The address at position a is actually called "virtual base table pointer", which points to "virtual base table". The virtual base table stores two data. The first is the reserved storage offset for the polymorphic virtual table, and the second is the offset between the base class object (here class B and class C) and the common base class (here class a) in the subclass.

As shown in the figure:

Supplement:

This virtual base table is very useful. It can not only make class B and class C objects share A class A member variable. Let's give an example.

When a class D object is assigned to a class C object, the slicing behavior occurs. But it should have been stored_ The position of a is replaced by the virtual base table pointer. At this time, the virtual base table pointer will play a role in slicing, and it will be found through the offset_ a. Then slice to the C base class object.

D d;
C c = d; // Occurrence slice

3. Summary

Although virtual inheritance can solve the problem of diamond inheritance, it makes the object model extremely complex, is not friendly to learners, and will also cause a certain loss of time efficiency. So in general, don't design diamond inheritance.

8, Inheritance and composition

Inheritance is an is-a relationship, which only occurs in subclasses and superclasses. Composition is a has-a relationship, that is, there can be another object in one object.

For example:

It can be said that Volkswagen is a kind of car, which is the relationship of is-a, so it is best to use inheritance.

class Car {
protected:
	string _color; // Car color
	string _num;   // license plate number
};

class SVW : public Car {
public:
	void show() {
		cout << "I am SVW" << endl;
	}
};

The car has four tires, which is the has-a relationship, so it is best to use a combination.

class Tyre {
protected:
	string _size; // size
};

class Car {
protected:
	string _color; // Car color
	string _num;   // license plate number
    Tyre _tpres;   // tyre
};

How to choose inheritance and composition?

You need to observe the relationship between two or more classes.

If it constitutes the relationship of is-a, that is, when A is B, inheritance is best used.

If the relationship of has-a is formed, that is, when there is B in A, it is best to use only combination.

If both relationships can be, either is-a or has-a, the combination is preferred.

Comparison of inheritance and combination

1. Inheritance allows you to define the implementation of derived classes according to the implementation of the base class, because the base class is transparent to subclasses** This reuse by generating derived classes is often referred to as "white box reuse"** In the inheritance mode, the internal details of the base class are visible to the derived class. Inheritance destroys the encapsulation of the base class to a certain extent. The change of the base class has a great impact on the derived class. The dependency relationship between the derived class and the base class is very strong and the coupling degree is high.

2. Composition is another reuse option besides class inheritance. Many functions can be obtained by combining objects. Object composition requires that the combined objects have well-defined interfaces. This reuse style is called black box reuse, because the internal details of the objects are invisible, the objects only appear in the form of "black box", there is no strong dependency between the combined classes, and the coupling degree is low.

3. In * * software engineering design, we emphasize "high cohesion and low coupling", that is, there is no correlation between the two classes as far as possible, so that we don't have to suffer from the pain of one hair and the whole body when we finally reconstruct the code** So if you can use combination, use combination first. But this does not mean that inheritance is not important, but we should have a value ranking in our hearts.

Common written interview questions

What is diamond inheritance? What is the problem of diamond inheritance?

Diamond inheritance is a special case of multiple inheritance. Two subclasses inherit the same parent class, and a class inherits the two subclasses at the same time. This inheritance is diamond inheritance.

After diamond inheritance, there will be two members of the parent class in the child class object, which will lead to data redundancy and ambiguity.

What is diamond virtual inheritance? How to solve data redundancy and ambiguity?

Virtual inheritance means that in the inheritance of diamond inheritance, we use virtual to inherit the same base class. In this way, only one base class object of the base class will exist in the last inherited subclass.

This is accomplished by using the virtual base table pointer and virtual base table. The virtual base table records the distance from class B and class C to class A object members. In general, only one class A member can be stored in class D, which solves the problems of ambiguity and data redundancy.

The difference between inheritance and composition? When to use inheritance? When to use combination?

Inheritance is an is-a relationship, while composition is an has-a relationship. If the relationship between two classes is is-a, inheritance is used; If the relationship between two classes is has-a, use combination; If the relationship between two classes can be regarded as both is-a and has-a, combination is preferred.

Keywords: C++

Added by dominant on Sun, 24 Oct 2021 21:21:41 +0300