[C++] Inheritance

Time:2023-10-13

[C++] Inheritance

I. Concept and definition of succession

1.1 Concept of succession

Inheritance mechanism is the most important means of object-oriented programming to make code reusable, which allows programmers to extend and add functionality while maintaining the characteristics of the original class, which produces new classes called derived classes. Inheritance presents a hierarchical structure of object-oriented programming, reflecting the cognitive process from simple to complex. Previously, we were exposed to reuse are function reuse, inheritance is the reuse of class design.

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // Name
	int _age = 18; // Age
};

class Student : public Person
{
protected:
	int _stuid;//student number
};

class Teacher : public Person
{
protected:
	int _jobid;//job number
};

int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

After inheritance, the members of the parent class Person (member functions + member variables) become part of the child class. This is reflected in the fact that Student and Teacher reuse members of Person; Student inherits its parent’s members and has its own attribute, _stuid, which represents the student’s student number; Teacher inherits its parent’s members and has its own attribute, _jobid, which represents the teacher’s job number.

[C++] Inheritance

1.2 Definition of Succession

1.2.1 Defining formats

Inheritance is defined in the following format, where Person is the parent class, also known as the base class, and Student is the child class, also known as the derived class.

[C++] Inheritance

1.2.2 Inheritance and access qualifiers

[C++] Inheritance

1.2.3 Changes in the way members of inherited base classes are accessed

Class Membership/Inheritancepublic Inheritanceprotected inheritanceprivate inheritance
public member of the base classpublic member of a derived classProtected Members of Derived ClassesPrivate Members of Derived Classes
protected members of the base classProtected Members of Derived ClassesProtected Members of Derived ClassesPrivate Members of Derived Classes
private members of the base classNot visible in derived classesNot visible in derived classesNot visible in derived classes

summarize

  • Base class private members are invisible to derived classes regardless of how they are inherited. Invisible means that the private members of the base class are still inherited into the derived class object, but the syntax restricts the derived class object from accessing them either inside or outside the class.

  • A private member of a base class cannot be accessed in a derived class. if a member of a base class does not want to be directly accessed outside the class, but needs to be accessible in the derived class, it is defined as protected. you can see that the protected member qualifier came about because of inheritance.

  • Summarizing the above table will show that the private members of the base class are invisible in the subclasses. Other members of the base class are accessible in subclasses by == Min (member’s access qualifier in the base class, inheritance method), public > protected > private.

  • The default inheritance method is private when the keyword class is used, and public when struct is used, but it is better to show the inheritance method.

  • In practice, the general use of public inheritance, almost rarely use protected and private inheritance, and do not advocate the use of protected and private inheritance, because protected and private inheritance down the members can only be used in the derived class inside the class, the actual extension of the maintenance is not strong.

II. Base Class and Derived Class Object Assignment Conversion

  • derived class object (computing)can be assigned toObjects of the base classPointers to base classesBase class references. There is a figurative term here called slicing or cutting. It means to cut the part of the parent class in the derived class and assign it to the past.

  • Base class objects cannot be assigned to derived class objects.

  • base classthepointer on a gaugeorquotePointers or references to derived classes can be assigned by forced type conversion. However, it is only safe if the pointer to the base class points to an object of the derived class. If the base class is polymorphic, you can use dynamic cast of RTTI (Run – Time Type Information) to recognize it and convert it safely.

[C++] Inheritance
Tips: This assignment of a subclass object to a parent object is also called an upconversion. Assigning a parent object to a child object is also called a downward conversion and is not allowed. Assigning a subclass object to a parent object is a type conversion that does not create intermediate temporary variables.

class Person
{
protected:
    string _name; // Name
    string _sex; // gender
    int _age; // age
};
class Student : public Person
{
public:
    int _No; // Student number
};

void Test()
{
    Student sobj;
    // 1. Subclass objects can be assigned to parent class objects/pointers/references
    Person pobj = sobj;
    Person* pp = &sobj;
    Person& rp = sobj;
	
	//2. Base class objects cannot be assigned to derived class objects
    //sobj = pobj;
}

Tips: rp is now an alias for the parent member of the subclass object, and does not create an intermediate temporary variable. Similarly, pp also points to the subclass object sobj, but the type of the pointer determines the member variables it can access, because pp is a pointer to the parent class, so pp can only access that part of the parent class in the subclass object.

[C++] Inheritance
Tips: You can use rp to modify that part of the parent’s member variables in the subclass object it points to.

[C++] Inheritance

class Person
{
protected:
    string _name; // Name
    string _sex; // gender
    int _age; // age
};
class Student : public Person
{
public:
    int _No; // Student number
};

void Test()
{
    Student sobj;
    // 1. Subclass objects can be assigned to parent class objects/pointers/references
    Person pobj = sobj;
    Person* pp = &sobj;
    Person& rp = sobj;
	
	//2. Base class objects cannot be assigned to derived class objects
    //sobj = pobj;

    //3. Pointers to base classes can be assigned to pointers to derived classes by forced type conversion.
    pp = &sobj;
    Student * ps1 = (Student*)pp; // This case is fine when converted.
    ps1->_No = 10;

    pp = &pobj;
    Student* ps2 = (Student*)pp; // This case converts fine, but there will be problems with out-of-bounds accesses
    ps2->_No = 10;
}

Tipsps2->_No = 10; would result in out-of-bounds access, and theps1->_No = 10; does not result in out-of-bounds access becausepp The initial pointing of a pointer, and the type of the pointer determines the amount of memory space that the pointer can access. Take the above as an example.pp The pointer initially points to a subclass objectsobjfurthermorepp pointer is a parent class pointer, which dictates that thepp The pointer can only access that portion of the subclass object’s member variables that are inherited from the parent class, and then thepp pointer performs a forced type conversion, assigning it to a subclass pointerps1at this timeps1 or pointing to a subclass objectsobjButps1 Instead, the type of theStudentThat’s what determinesps1 have access tosobj in all member variables (inherited from the parent class and subclass-specific); and the second type conversion, thepp The pointer acts as a parent class pointer, initially pointing to a parent class objectpobj This is no problem and will be followed bypp The pointer performs a forced type conversion to assign to a subclass pointerps2 at this timeps2 It’s still the parent class.pobj address that points to thepobjbut its type is subclassedStudentThe type of the object determines its access scope. It can supposedly access all the members of a subclass object, but it points to a parent object, which does not have the member variables of the subclass._No, although its type dictates that it can access that member variable, it is not present in the parent class object, which ultimately leads to out-of-bounds access issues.

III. Scope in inheritance

  • In an inheritance system base and derived classes have separate scopes.

  • A subclass and parent class have members with the same name, the subclass member will block the direct access of the parent class to the member with the same name, this situation is called hiding, also called redefining. (In subclass member functions, you can use base class::base class member Show access)

  • Note that in the case of member function hiding, only the function name needs to be the same to constitute hiding.

  • Note that inside the actual inheritance, it is better not to define members with the same name.

TipsThe compiler will search for a variable in the local domain of the current member function first, and if it doesn’t find it, it will search in the class domain of the current member function next.

// Student's _num and Person's _num form a hidden relationship, so you can see that this code runs, but is very confusing.
class Person
{
protected:
    string _name = "Little Lee"; // Name
    int _num = 111; // identity card number
};
class Student : public Person
{
public:
    void Print()
    {
        cout << "Name :" << _name << endl;
        cout << " ID number:" << Person::_num << endl;//Specify to go to the parent class to find the
        cout << " student number:" << _num << endl;//not specified, now look in the local domain, there is no _num in the local domain, next go to the current class domain to find it
    }
protected:
    int _num = 999; // school number
};

[C++] Inheritance

// The fun in B and the fun in A are not overloads because they are not in the same scope.
// A fun in B and a fun in A constitute a hide, and a member function constitutes a hide if it satisfies the same function name.
class A
{
public:
    void fun()
    {
        cout << "func()" << endl;
    }
};
class B : public A
{
public:
    void fun(int i)
    {
        A::fun();
        cout << "func(int i)->" << i << endl;
    }
};

void Test()
{
    B b;
    //b.fun()//this won't work!
    b.A::func()//
    b.fun(10);
};

[C++] Inheritance
Tips: The premise of function overloading is the same scope. Overloading the underlying use of the function name modifier rules, in the same scope of the same function if you do not use the function name modifier rules, the compiler will not be able to distinguish between the two functions of the same name, and for the different scopes of the two functions of the same name, the compiler will be directly based on the domain of the rules of the search to distinguish between them. To summarize: as long as the function name is the same in the parent-child class domain, it constitutes hidden. As shown in the code above, theb.fun() is not allowed, and the compiler sees the subclass object calling thefun function, it will now look for it in the subclass, and if it finds it but finds that it is missing a parameter, the compiler will report an error. A subclass object that wants to call a hidden (redefined) function in the parent class can do so by specifying the class field.

IV. Default Member Functions in Derived Classes

6 default member functions, “default” means that we do not write, the compiler will automatically generate one for us, so in the derived class, how are these member functions generated?

  • The constructor of the derived class must call the constructor of the base class to initialize that portion of the base class’s members. If the base class does not have a default constructor, the call must be shown during the initialization list phase of the derived class constructor.

  • The copy constructor of a derived class must call the copy construct of the base class to complete the copy initialization of the base class.

  • derived classoperator= It is necessary to call the base class’soperator= Complete the copy of the base class.

  • The destructor of the derived class automatically calls the destructor of the base class to clean up the base class members after it has been called. This is because it ensures that derived class objects clean up the derived class members before the base class members.

  • Derived class object initialization calls the base class construct before calling the derived class construct.

  • Derived class object destructuring cleans up by calling the derived class destructor before calling the base class destructor.

  • Because of some subsequent scenarios destructor functions need to constitute a rewrite, one of the conditions of rewriting is that the function name is the same. Then the compiler will make a special treatment of the destructor function name, processing it asdestrutor()so the parent function does not add thevirtual case, the subclass destructor and the parent destructor form a hidden relationship.

4.1 Default Constructor

// Default constructor
class Person
{
public:
    Person(const char* name = "Peter", const char* sex = "male ")
        :_name(name)
        ,_sex(sex)
    {
        cout << "Person()" << endl;
    }
protected:
    string _name; // Name
    string _sex;
};

class Student : public Person
{
public:
    Student(const char* name = "Jang SAN ", const char* sex =" male ", int num = 0)
        :_num(num)
        ,Person(name, sex)
        //, _name(name)// This is wrong.
    {
        cout << "Student()" << endl;
    }
protected:
    int _num;//student number
};
void Test()
{
    Student s;
}

Tips: You can’t initialize a separate base class member variable in the initialization list of a derived class constructor, i.e. _name(name) is not allowed, and can only be initialized as a whole, like thePerson(name, sex) This way. The fact that the second will go ahead and call the constructor of the base class indicates that the member variables inherited from the base class must be declared in front of the member variables of the derived class.

4.2 Copy constructors

//copy constructor
class Person
{
public:
    // Default constructor
    Person(const char* name = "Peter", const char* sex = "male ")
        :_name(name)
        ,_sex(sex)
    {
        cout << "Person()" << endl;
    }

    //copy constructor
    Person(const Person& p)
        :_name(p._name)
        ,_sex(p._sex)
    {
        cout << "Person(const Person& p)" << endl;
    }

protected:
    string _name; // Name
    string _sex;//gender
};

class Student : public Person
{
public:
    // Default constructor
    Student(const char* name = "Jang SAN ", const char* sex =" male ", int num = 0)
        :_num(num)
        ,Person(name, sex)
        //, _name(name)
    {
        cout << "Student()" << endl;
    }

    //copy constructor
    Student(const Student& s)
        :Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }
    
protected:
    int _num;//student number
};

void Test()
{
    Student s("Zhang Wei", "male", 213);//call the default constructor
    Student s1(s);//call the copy constructor

    Student s2;
    s2 = s1;//call assignment operator overloading

    //Person p = s1;
}

Tips: The copy constructor of a derived class must also call the constructor of the base class, like soPerson(s), where one of the points we mentioned above is used, namely that a derived class object can be assigned a reference to a base class that is an alias for that part of the base class member variable in the derived class object. Here, if you don’t writePerson(s)Although this is a copy constructor, the compiler will by default call the default constructor of the base class, not the copy constructor of the base class.

4.3 Assignment Operator Overloaded Functions

// Assignment operator overloading
class Person
{
public:
    // Default constructor
    Person(const char* name = "Peter", const char* sex = "male ")
        :_name(name)
        ,_sex(sex)
    {
        cout << "Person()" << endl;
    }

    //copy constructor
    Person(const Person& p)
        :_name(p._name)
        ,_sex(p._sex)
    {
        cout << "Person(const Person& p)" << endl;
    }

    // Assignment operator overloading
    Person& operator=(const Person& p)
    {
        if (this != &p)
        {
            cout << "Person& operator=(const Person& p)" << endl;
            _name = p._name;
            _sex = p._sex;
        }
        
        return *this;
    }

protected:
    string _name; // Name
    string _sex;//gender
};

class Student : public Person
{
public:
    // Default constructor
    Student(const char* name = "Jang SAN ", const char* sex =" male ", int num = 0)
        :_num(num)
        ,Person(name, sex)
        //, _name(name)
    {
        cout << "Student()" << endl;
    }

    //copy constructor
    Student(const Student& s)
        :Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }

    // Assignment operator overloading
    Student& operator=(const Student& s)// overloading with parent class assignment operator constitutes hide
    {
        if (this != &s)
        {
            cout << "Student& operator=(const Student& s)" << endl;
            Person::operator=(s);
            _num = s._num;
        }

        return *this;
    }
protected:
    int _num;//student number
};

void Test()
{
    Student s("Zhang Wei", "male", 213);//call the default constructor
    Student s1(s);//call the copy constructor

    Student s2;
    s2 = s1;//call assignment operator overloading
}

Tips: You need to be careful with assignment operator overloading, because the assignment operator overloads functions whose names are alloperator=Therefore, the copy operator overloaded function of the parent class and the assignment operator overloaded function of the child class form a hidden (redefined) relationship. So in order to call the parent’s assignment operator overloaded function in the subclass’s assignment operator overloaded function, you need to specify the class domain, like soPerson::operator=(s);If you don’t specify a class field, the compiler will by default call the subclass’s own assignment operator overload, which will result in infinite recursion and a stack overflow.

4.4 Analyzing Functions

class Person
{
public:
    // Default constructor
    Person(const char* name = "Peter", const char* sex = "male ")
        :_name(name)
        ,_sex(sex)
    {
        cout << "Person()" << endl;
    }

    //copy constructor
    Person(const Person& p)
        :_name(p._name)
        ,_sex(p._sex)
    {
        cout << "Person(const Person& p)" << endl;
    }

    // Assignment operator overloading
    Person& operator=(const Person& p)
    {
        if (this != &p)
        {
            cout << "Person& operator=(const Person& p)" << endl;
            _name = p._name;
            _sex = p._sex;
        }
        
        return *this;
    }

    //Destructor
    ~Person()
    {
        cout << "~Person()" << endl;
    }

protected:
    string _name; // Name
    string _sex;//gender
};

class Student : public Person
{
public:
    // Default constructor
    Student(const char* name = "Jang SAN ", const char* sex =" male ", int num = 0)
        :_num(num)
        ,Person(name, sex)
        //, _name(name)
    {
        cout << "Student()" << endl;
    }

    //copy constructor
    Student(const Student& s)
        :Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }

    // Assignment operator overloading
    Student& operator=(const Student& s)// overloading with parent class assignment operator constitutes hide
    {
        if (this != &s)
        {
            cout << "Student& operator=(const Student& s)" << endl;
            Person::operator=(s);
            _num = s._num;
        }

        return *this;
    }

    //Destructor
    ~Student()
    {
        cout << "~Student()" << endl;
        //Person::~Person();//showing the call causes the problem of destructuring twice
    }
protected:
    int _num;//student number
};

void Test()
{
    Student s("Zhang Wei", "male", 213);//call the default constructor
    Student s1(s);//call the copy constructor

    Student s2;
    s2 = s1;//call assignment operator overloading

    Person p = s1;// call the copy constructor of the parent class directly
}

TipsFor the destructor, in order to ensure the order of destructuring (for a subclass object, the parent part of it calls construct first, and the subclass part of it calls construct later, and in terms of the order of creation of the stack frames, the constructed one has to be destructed first, so it needs to be destructed first to clean up the resources of the subclass, and then go to the parent’s destructor function to clean up the portion of the resources of the parent class in the subclass object), the compiler will automatically call the The compiler will automatically call the parent’s destructor, so we don’t need to show ourselves calling the parent’s destructor. Second, because of the polymorphism, the function name of the destructor is handled in a special way, uniformly asdestructorTherefore, the parent’s destructor and the child’s destructor essentially form a hidden (redefined) relationship, and if you want to show the call to the parent’s destructor in the child’s destructor, you need to specify the class domain, just as you do for assignment operator overloaded functions. But note! Notice! We don’t need to display the parent’s destructor in the subclass’s destructor, even if we do, the compiler will automatically call the parent’s destructor, which will cause the parent’s portion of the subclass to be freed twice, which will cause problems. There is another reason to destruct the subclass first, that is, the destructor of the subclass can use the member variables of the parent class, if you call the destructor of the parent class first, then you can no longer use the member variables of the parent class in the destructor of the subclass. It is not possible to call a member variable of a subclass in the parent class’s destructor, so there is no problem with calling the parent class’s destructor first.

V. Succession and Friends of the Dollar

Friendships are not inheritable, which means that friend functions declared in the base class cannot access private and protected members in subclasses.

//Friendships cannot be inherited
class Student; // Declare first
class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name = "Peter"; // Name
};
class Student : public Person
{
protected:
    int _stuNum = 1111; // school number
};
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    //cout << s._stuNum << endl;// is not a friend of the subclass, so you cannot access subclass member variables in a function
    cout << s._name << endl;
}
void Test()
{
    Person p;
    Student s;
    Display(p, s);
}

Tips: As shown in the code above, theDisplay function is simply the parent classPerson The friendly metafunctions of a subclassStudent The friendly metafunction ofDisplay function can only call to member variables in the parent class, not in the child class, i.e., in theDisplay in a functions._stuNum is not allowed. It is also possible to call that part of a subclass object that is a member variable of the parent class.

VI. Inheritance and static member variables

If the base class defines a static member, there is only one such member in the entire inheritance system. No matter how many subclasses are derived, there is only one instance of the static member.

class Person
{
public:
    Person() { ++_count; }
protected:
    string _name; // Name
public:
    static int _count; // Count the number of people.
};

int Person::_count = 0;
class Student : public Person
{
protected:
    int _stuNum; // student number
};

class Graduate : public Student
{
protected:
    string _seminarCourse; // Subject of study
};

void Test()
{
    Student s1;
    Student s2;
    Student s3;
    Graduate s4;
    cout << "Number of people :" << Person::_count << endl;
    Student::_count = 0;
    cout << "Number of people :" << Person::_count << endl;
}

[C++] Inheritance

Tips: The static member belongs to the parent class and the derived class (they share it), there is no separate copy of it in the derived class, what is inherited is the right to use the static member. The above code uses a static member variable_count to count the number of parent and child objects created, simply execute the parent constructor’s++_count That’s all, because the process of creating the object of the subclass goes and calls the constructor of the parent class.

VII. Complex rhombic inheritance and rhombic virtual inheritance

single inheritance: A subclass with only one direct parent is said to have a single inheritance relationship.

[C++] Inheritance
multi-inheritance: A subclass with two or more direct parents is said to be in a multiple inheritance relationship.

[C++] Inheritance
rhombic inheritance: Diamond inheritance is a special case of multiple inheritance.

[C++] Inheritance
Problems with rhombic inheritance: From the following object member model construct, you can see that diamond inheritance has data redundancy and duality issues. In theAssistant objectsPerson There will be two copies of the member variables of the

[C++] Inheritance

class Person
{
public:
    string _name; // Name
    int _age;//age
};

class Teacher : public Person
{
protected:
    int _id;//employee number
};


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


class Assistant : public Teacher, public Student
{
protected:
    string _majorCourse; // Major course
};

void Test()
{
    Assistant a;
    a._age = 10;
}

[C++] Inheritance
Tips:: To the_age Unclear access is actually a duality problem. To solve the duality problem, we can access it by specifying the class domain, like the following.

void Test()
{
    Assistant a;
    a.Teacher::_age = 10;
    a.Student::_age = 18;
}

[C++] Inheritance
Tips: This solves the duality problem, but it is illogical for an object-oriented language, because a person cannot have two ages at the same time. And data redundancy is still not solved. Therefore, the following need to introduce virtual inheritance, virtual inheritance can solve the diamond inheritance of the problem of duality and data redundancy. As in the above inheritance relationship, when Student and Teacher inherit Person, virtual inheritance can be used to solve the problem. It is important to note that virtual inheritance should not be used elsewhere.

class Person
{
public:
    string _name = "Peter"; // Name
    int _age = 0;//age
};

class Teacher : virtual public Person
{
protected:
    int _id = 0;//employee number
};

class Student : virtual public Person
{
protected:
    int _num = 0;//Academic title
};

class Assistant : public Teacher, public Student
{
protected:
    string _majorCourse; // Major course
};

[C++] Inheritance

7.1 Principles of Virtual Inheritance for Solving Data Redundancy and Duality

In order to study the principle of virtual inheritance, we give a simplified rhombic inheritance system and then observe the model of object members with the help of a memory window.

class A
{
public:
    int _a;
};

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

class C : public A
//class C : virtual public A
{
public:
    int _c;
};
class D : public B, public C
{
public:
    int _d;
};

void Test()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
}

[C++] Inheritance
[C++] Inheritance
Tips: The figure above models the object members of ordinary diamond inheritance in memory. It can be seen that the memory stores two_a member variables, one inherited from B and the other inherited from C. Here is another look at the model of how the object members of virtual inheritance are stored in memory.

class A
{
public:
    int _a;
};

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

class C : virtual public A
//class C : virtual public A
{
public:
    int _c;
};
class D : public B, public C
{
public:
    int _d;
};

void Test()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    d._a = 10;
}

[C++] Inheritance
Tips: The model for storing the members of a virtually inherited object in memory changes dramatically. First member variables of type A-a from types B and C. Plain diamond inheritance._a will store one copy each in types B and C, resulting in data redundancy and dichotomy, whereas in diamond inheritance, a member variable of type A_a Only one copy will be stored in the memory space. Another change is that there is one more piece of data in each of the types B and C, as in the B00ff7bdc and in C00ff7be4These two are obviously addresses, according to the address to find the corresponding memory space to store the data, you can find that the B type of this address space to store a 14, where 14 is hexadecimal, converted to decimal is 20, and 20 is exactly the first address of the B type of0x00EFFBA0 and type A first address0x00EFFBB4 The distance between. Similarly, this address space in class C stores the0c Converted to decimal is 12, which is the first address of the C type.0x00EFFBA8 and the first address of type A0x00EFFBB4 The distance between them. Here there may be partners want to ask, why not this relative distance directly in the D-type object, but a separate block of space in memory to store. First of all, because we need to store not only the relative distance, but also some other information. Secondly, we may create multiple D objects at the same time, for these multiple D objects, their offsets are the same, if we store a copy of it in all D type objects, it will be a great waste when the data volume is large, so we can extract this part of the “fixed” information, and then open a space to store it in the memory, and then store it in the D type objects. space to store, and then in the D-type objects can be stored on the address of the space, this can be done in the large amount of data, you can effectively save space, improve memory utilization.

[C++] Inheritance
Tips: As shown above, we have created two objects of type D, d and d1, both of which have the same address in memory.0x00337bdc respond in singing0x00337be4These two address spaces store the relative distance between type B and type A, and the relative distance between type C and type A, respectively. The space where the offsets are stored is also known as the virtual base table (the table where the base class offsets are found), but it should be noted that not only offsets are stored in the virtual base table, but also some other information, which will be revealed in the next article on polymorphism, so if you are interested in it, you may want to click on a concern first.

7.2 Meaning of storage offsets

void Test()
{
    D d;
    d._a = 10;
}

As shown in the above code, defining an ordinary D-type object d, and then accessing its member variable _a, in this case the offset is not used. In this case, the offset is not used. The function of the offset is to find the member of the “grandfather class” of the D object (i.e., the parent of the parent class, which is class A). For the ordinary D object d, when defining the object, the compiler will create stack frames for the members according to the order of the declarations (allocating the space for storage in order), so the compiler knows where the member variable _a is stored, so when we execute the code above, we can access the member variable _a in it. a member variable is stored there, so when we execute thed._a = 10 The compiler will find this memory space directly and does not need to look up the offset through the virtual base table. The real purpose of storing offsets is for cases like the following.

void Test()
{
    D d;
    d._a = 1;
    d._b = 2;
    d._c = 3;
    d._d = 4;

    B b;
    b._a = 1;
    b._b = 2;

	B* ptr = &b;
    ptr->_a++;

    ptr = &d;
    ptr->_a++;
}

Tips: Here we first need to make it clear that in a virtual inheritance system, the model of storing B object members in memory also changes compared to normal inheritance, and it also involves a virtual base table.

[C++] Inheritance
With that clear, let’s move on to the code above, where we define a pointer of type BptrThe pointer can point to an object of type B or to an object of type D (note that it can only access the members of B in the object of type D). Althoughptr can point to objects of different types, butptr is always of type B, which dictates that no matter what youptr pointing to any type of object, you can only access members that are available in class B, i.e., theptr Maximum access to_a respond in singing_b These two members.ptr As a pointer variable, it doesn’t know who it’s pointing to after it’s converted to an instruction, it just stores an address, and if theptr is the address of an object of type B, then its_a respond in singing_b is contiguous in memory space, but if theptr is the address of an object of class D, then its_a respond in singing_b It is not contiguous in memory space. There may be some other type in between. And the way pointers work is that firstly the pointer must store the first address of a variable, and secondly the type of the pointer determines how many contiguous spaces it can access starting from the first address of that variable. Takeint type pointer, for example, he can access up to four consecutive bytes for theint type of pointer +1 automatically skips four bytes. Going back to this, when theptr The first address of an object of class D is stored.ptr The members that can be accessed are not contiguous, then the pointer cannot find the_a up, at which point the usefulness of storing offsets comes into play, theptr One can look through the virtual base table and find the offset, which in turn finds the_a Membership. At this point in time, no matter whatptr is pointing to an object of type B or to an object of type D, when theptr To visit_a are converted to taking the offset first before calculating the_a address in the object before accessing it.

7.2 Virtual Inheritance to Solve Data Redundancy Problems

Take the above code as an example, if the size (number of bytes) of the member variables related to type A in a D-type object is relatively small, then the size (number of bytes) of this D-type object under the virtual inheritance system may be larger than that of the D-type object created under the normal inheritance system, the size of a D-type object under the normal inheritance system in the code above is 20 bytes, and the size of a D-type object under the virtual inheritance system is 24 bytes. The above code has a size of 20 bytes for a D-type object under normal inheritance and 24 bytes under virtual inheritance.

[C++] Inheritance
This virtual inheritance system created under the object than under the ordinary inheritance system created under the object of the main reason is that the A type of member variables are too few, the occupied memory space is too small, resulting in virtual inheritance of the expenditure is greater than the benefit, if the A in the member variables are more, or a large array, then the virtual inheritance to solve the ordinary inheritance system of the redundancy of the data can be reflected in the function of the inheritance system.

VIII. Summary and reflection on succession

Many of my friends feel that C++ syntax is complex, in fact, multiple inheritance is a reflection. With multiple inheritance, there is diamond inheritance, with diamond inheritance there is data redundancy and duality, in order to solve the problem of data redundancy and duality, and the introduction of diamond virtual inheritance, the underlying implementation is very complex. Therefore, it is generally not recommended to design a multiple inheritance, must not design a diamond-shaped inheritance. Otherwise, there are problems in complexity and performance. Multi-inheritance can be considered one of the defects of C++, and later many OO languages do not have multi-inheritance, such as Java.

8.1 Inheritance and combinations

  • public Inheritance is an is-a relationship. That is, every derived class object is a base class object.

  • A combination is a has-a relationship. That is, a member variable of type A is declared in class B. Thus every B object has an A object in it.

  • Prioritize the use of object combinations over inheritance.

  • Inheritance allows us to define the implementation of a derived class based on the implementation of the base class. This reuse by generating derived classes is often referred to as white-box reuse. The term “white box” is used in relation to visibility: in an inheritance approach, the internal details of the base class are visible to the subclasses. Inheritance destroys the encapsulation of the base class to some extent, and changes to the base class have a significant impact on the derived class. The dependency between the derived class and the base class is strong and coupled.

  • Object combination is an alternative to class inheritance for reuse. New and more complex functionality can be obtained by grouping leaders or combining objects. Object composition requires that the objects being combined have well-defined interfaces. This style of reuse is called “black box reuse” because the internal details of the object are not visible. The objects are simply “black boxes”. There are no strong dependencies between the combined classes and the coupling is low. Prioritizing the use of object composition helps us to keep each class encapsulated.

  • Practically try to go with combinations as much as possible. Combinations have low coupling and good code maintenance. However, inheritance also has a place, some relationships are suitable for inheritance that will use inheritance, in addition to the realization of polymorphism, you must also inherit.

IX. Succession of common interview questions

// What is the output of this code below?
class A {
public:
    A(const char* s) 
    { 
        cout << s << endl; 
    }

    ~A() 
    {}
};

class B :virtual public A
{
public:
    B(const char* s1, const char* s2) 
        :A(s1) 
    { 
        cout << s2 << endl; 
    }
};

class C :virtual public A
{
public:
    C(const char* s1, const char* s2) 
        :A(s1) 
    { 
        cout << s2 << endl; 
    }
};

class D :public B, public C
{
public:
    D(const char* s1, const char* s2, const char* s3, const char* s4) 
        :B(s1, s2)
        ,C(s1, s3)
        ,A(s1)
    {
        cout << s4 << endl;
    }
};

int main() {
    D* p = new D("class A", "class B", "class C", "class D");
    delete p;
    return 0;
}

[C++] Inheritance

TipsThe key to solving this problem is that we need to know the following two points. First point: for virtual inheritance, although class D seems to inherit only class B and class C, it is a kind of diamond-shaped inheritance, class B and class C inherit class A, so in a sense, class D also inherits class A. And because of this is a diamond-shaped virtual inheritance, there is only one copy of the member variable of class A in class D. Here, we need to call the constructor of class A first in the initialization list of constructor of class D. Therefore, if there is no default constructor of class A, we need to explicitly write a statement to call the constructor of class A in the initialization list. In the fourth subsection, we mentioned that the constructor of a derived class must call the constructor of the base class to initialize that part of the base class. The second point we need to grasp is that the initialization order of the member variables in the initialization list of the derived class constructor is in accordance with the order of inheritance, first call the constructor of the first inherited class, then call the second inherited class, and so on, and then finally initialize the member variables of the current class, take the above code as an example:class D :public B, public CHowever, it should be noted that this is a diamond-shaped virtual inheritance, and both class B and class C inherit from class A. Therefore, the compiler first calls the constructor of class A, then calls the constructor of class B, and then calls the constructor of class C. It should also be noted that, although the initialization lists of the constructors of class B and class C are shown as statements calling the constructor of class A, this is a diamond-shaped virtual inheritance, and the first step of creating D objects is to call the constructor of class A. Therefore, the compiler will not call the constructor of class B and class C in the process of calling the constructor of class B and class C again. Although the initialization list of the constructor of class B and class C shows that the statement of calling the constructor of class A is written, this is a diamond virtual inheritance, and the first step of creating the object of class D is to call the constructor of class A, so the compiler will not call the constructor of class A in the process of calling the constructor of class B and class C again. Although the compiler will not execute the statement that calls the constructor of class A in the constructor of class B and class C during the process of creating an object of class D, we can’t delete this statement (unless there is a default constructor of class A), because this statement will not be executed only during the process of creating an object of class D. We may also create an object of class B and an object of class C, and this statement will be executed at this time. With the above knowledge base it is not difficult to know the printout of the above code.

// What is the relationship between the three of them, p1, p2, and p3?
class Base1 
{ 
public:  
    int _b1; 
};

class Base2 
{ 
public:  
    int _b2; 
};

class Derive : public Base1, public Base2 
{ 
public: 
    int _d; 
};

int main() 
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    
	printf("p1:%p\n", p1);
    printf("p2:%p\n", p2);
    printf("p3:%p\n", p3);
    return 0;
}

[C++] Inheritance
Tips: The result here has to do with the order of inheritance.p3 As a pointer to a Derive type, it must be a pointer to thed The first address of the object must be the smallest address number in the memory space where the current object is stored, i.e. at the low address. From the above code, we can see that the Derive object inherits Base1 first, which determines that a Derive objectd In memory Base1 member variables must be stored at the top, i.e., at the low address, which is the first address of the Derive object.p1 As a Base1 type pointer, it can only point to thed object inherits member variables from Base1, and Base1’s member variables reside in the memory space with the smallest address number.p1 == p3Derive’s second inheritance is Base2, and the space to store Base2’s member variables goes sequentially backward, so naturally it is notd which results in the first address of thep1 == p3 != p2

X. Conclusion

This is the end of today’s sharing! If you think the article is still good, you cantripleIn support of that.Haruto’s homepageThere are a lot of interesting articles, welcome partners to go ahead and comment, your support is the power of the spring people to move forward!

[C++] Inheritance

Recommended Today

uniapp and applet set tabBar and show and hide tabBar

(1) Set the tabBar: uni.setTabberItem({}); wx.setTabberItem({}); indexnumberisWhich item of the tabBar, counting from the left, is indexed from 0.textstringnoButton text on tabiconPathstringnoImage PathselectedIconPathstringnoImage path when selectedpagePathstringnoPage absolute pathvisiblebooleannotab Whether to display uni.setTabBarItem({ index: 0, text: ‘text’, iconPath: ‘/path/to/iconPath’, selectedIconPath: ‘/path/to/selectedIconPath’, pagePath: ‘pages/home/home’ }) wx.setTabBarItem({ index: 0, text: ‘text’, iconPath: ‘/path/to/iconPath’, selectedIconPath: ‘/path/to/selectedIconPath’, pagePath: ‘pages/home/home’ }) […]