C++【In-depth understanding of inheritance】

1. Inheritance concept and definition

1. The concept of inheritance:
Inheritance is the most important concept in object-oriented programming. It allows us to define a class based on another class. It allows programmers to extend and add functions while maintaining the characteristics of the original class. This also achieves the effect of reusing code functions and improving execution efficiency. When creating a class, there is no need to rewrite new data members and member functions. You only need to specify that the new class inherits the members of an existing class. This existing class is called a base class, and a new class is generated. Classes are called derived classes. Inheritance presents the hierarchy of object-oriented programming.
Inheritance represents the is a relationship. For example, students are people. As shown below
insert image description here

class person {
    
    
public:
    void sleep() 
    void walk()
protected:
string _name = "nza";
int _age = 21;  
    
};
//派生类
class student : public person {
    
    
public:
    void study()
protected:
int _ID;
};
int main()
{
    
    
    Student s;
    s.walk();
   return 0;
}

After inheritance, the members (member functions + member variables) of the person of the parent class will become part of the subclass. This shows that the student reuses the members of the person. You can also use the monitoring window to view students, and you can see the reuse of variables. You can see the reuse of member functions by calling walk.
2. Definition format:
the person above is the parent class, also called the base class, and the student is the subclass, also called the derived class.
3. Inheritance methods:
three types: public inheritance, protected inheritance, and private inheritance.
4. Access qualifiers:
public access, protected access, private access.
5. Access control and inheritance:
Derived classes can access all non-private members of the base class. Therefore, if the base class members do not want to be accessed by the member functions of the derived class, they should be declared as private in the base class.
insert image description here
A derived class inherits all base class methods, with the following exceptions:
base class constructors, destructors, and copy constructors.
Overloaded operators for the base class.
Friend function of the base class.
6. Changes in access methods of inherited base class members
insert image description here
Generally, protected or private inheritance is basically not used, and public inheritance is usually used. When using different types of inheritance, follow the following rules:
Public inheritance (public): When a class is derived from a public base class, the public members of the base class are also public members of the derived class, and the protected members of the base class are also derived classes. Private members of the base class cannot be directly accessed by derived classes, but can be accessed by calling the public and protected members of the base class.
Protected inheritance (protected): When a class is derived from a protected base class, the public and protected members of the base class become protected members of the derived class.
Private inheritance (private): When a class is derived from a private base class, the public and protected members of the base class become private members of the derived class.
Summary:
1. The private members of the base class are invisible no matter how they are inherited in the derived class. Invisibility here means that the private members of the base class are still inherited into the derived class object, but the derived class object is grammatically restricted from being able to access it no matter inside or outside the class.
2. The private members of the base class cannot be accessed in the derived class. If the base class member does not want to be accessed directly outside the class, but needs to be accessible in the derived class, it is defined as protected. It can be seen that the protected member qualifier is due to inheritance.
3. When using the keyword class, the default inheritance method is private, and when using struct, the default inheritance method is public, but it is best to write the inheritance method explicitly.
7. Scope in inheritance :
In the inheritance system, both the base class and the derived class have independent scopes.
There are members with the same name in the subclass and the parent class, and the subclass members will block the direct access of the parent class to the members with the same name. This situation is called hiding or redefinition. (In subclass member functions, base class::base class members can be used for explicit access) It should be noted that if the member function is hidden, only the same function name is required to constitute hiding. In practice it is best not to define members with the same name in the inheritance hierarchy.

class P
{
    
    
public:
void fun()
{
    
    
cout << "func()" << endl;
}
};
class S : public P
{
    
    
public:
void fun(int i)
{
    
    
    A::fun();
    cout << "A" <<i<<endl;
}
};
int main()
{
    
    
     S s;
     s.fun(1);
}

The fun in S and the fun in A do not constitute overloading, because they are not in the same scope, the fun in S and the fun in A constitute hiding, and the member functions that meet the same function name constitute hiding.
8. Inheritance and friends:
Friend relationships cannot be inherited, that is to say, base class friends cannot access private and protected members of subclasses.
If you must need a friendship relationship, you must declare it in both the parent class and the subclass.
9. Inheritance and static members:
If a class defines a static static member, there is only one such member in the entire inheritance system. No matter how many subclasses are derived, there is only one static member instance.

2. Base class and derived class object assignment conversion

First look at the object memory model in inheritance:
insert image description here
the derived class will retain all the attributes and behaviors of the base class, and each instance of the derived class contains a complete copy of the base class instance data.
Look at assignment conversion:
derived class objects can be assigned to objects of the base class or pointers to the base class or references to the base class. There is a vivid saying here called slicing or cutting. It means to assign values ​​to the parent class in the derived class, but the base class object cannot be assigned to the derived class object, and the pointer or reference of the base class can be assigned to the pointer or reference of the derived class through forced type conversion. But it must be safe when the pointer of the base class points to the derived class object. As shown in the figure below:
insert image description here
Because the number of members of the subclass must be >= the number of members of the parent class, when a subclass is assigned to the parent class, the subclass can assign the members belonging to the parent class to the parent class, and the remaining members of the parent class Class does not contain. In fact, it is to slice out the members of the subclass and assign them to the parent class. As shown in the figure above, the members in the parent class A object and the subclass B object assign the member slices in A that they contain to the parent class A object when assigning values.

Detailed diagram and code:
insert image description here

class A
{
    
    };
class B : public A
{
    
    };
int  main ()
{
    
    
  B sobj ;
 // 1.子类对象可以赋值给父类对象/指针/引用
  A pobj = sobj ;
  A* pp = &sobj;
  A& rp = sobj;
 
 //2.基类对象不能赋值给派生类对象
  sobj = pobj;
 
  // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
  pp = &sobj
  B* ps1 = (B*)pp; // 这种情况转换是可以的。
 
  pp = &pobj;
  B* ps2 = (B*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
}

Third, the default member function of the derived class

(1) Constructor

Member functions in the base class can be inherited and can be accessed through objects of the derived class, but this is only for ordinary functions, and cannot be inherited for the constructor and destructor of the base class. The constructor of the derived class must call the constructor of the base class to initialize that part of the members of the base class. If the base class has no default constructor, the call must be explicit during the initializer list phase of the derived class constructor.
In the following code, Person has a default constructor. In the constructor of the subclass, the default constructor of the parent class will be called first to complete the initialization of the members of the parent class, and then the constructor of the subclass will be called to initialize the members of the subclass.

class Person
{
    
    
public:
	//父类构造函数
	Person(string name = "父类")//父类有默认构造函数
		:_name(name)
	{
    
    
		cout << "Person()" << endl;
	}
protected:
	string _name;
};
class Student : public Person
{
    
    
public:
	//子类构造函数
	Student(string name, int num)
		:_num(num)
	{
    
    
		cout << "Student()" << endl;
	}
protected:
	int _num;
};

int main()
{
    
    
	Student s("nza", 1);

	return 0;
}

If the parent class does not have a default constructor and cannot be initialized without passing parameters, you need to explicitly call the parent class constructor in the subclass constructor to complete the initialization of the parent class members. You can use the initialization list to complete the task of the constructor. The initialization list can use the constructor of the base class to complete and populate the initialization of the data in the derived class. as follows:

class Person
{
    
    
public:
	//父类构造函数
	Person(string name)//如果不传参无法初始化
		:_name(name)
	{
    
    
		cout << "Person()" << endl;
	}
protected:
	string _name;
};
class Student : public Person
{
    
    
public:
	//子类构造函数
	Student(string name, int num)
		: Person(name)//调用父类构造函数初始化
		, _num(num)
	{
    
    
		cout << "Student()" << endl;
	}
protected:
	int _num;
};

int main()
{
    
    
	Student s("nza", 1);

	return 0;
}

In short, the calling order of the constructor is top-down according to the inheritance hierarchy, from the base class to the derived class. Regardless of the order of the initialization list, a derived class constructor always calls the base class constructor before executing other code.

(2) copy constructor

Like the idea of ​​the constructor, the copy constructor of the derived class must call the copy constructor of the base class to complete the copy initialization of the base class. When calling the copy construction of the parent class in the subclass, just pass in the subclass object directly, and the copy construction of the parent class will get the part of the parent class through "slicing".

class Person
{
    
    
public:
	//父类构造函数
	Person(string name = "父类")//父类有默认构造函数
		:_name(name)
	{
    
    
		cout << "Person()" << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{
    
    
		cout << "Person(const Person& p)" << endl;
	}

protected:
	string _name;
};
class Student : public Person
{
    
    
public:
	//子类构造函数
	Student(string name, int id)
		:_id(id)
		, Person(name)
	{
    
    
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)//直接传s,通过切片拿到父类的部分
		, _id(s._id)
	{
    
    
		cout << "Student(const Student& s)" << endl;
	}
protected:
	int _id;
};

int main()
{
    
    
	Student s("nza", 1);
	Student s2(s);//拷贝构造
	return 0;
}

(3) Assignment overloading

The operator= of the subclass must explicitly call the operator= of the parent class to complete the assignment of the parent class. Because the same function name constitutes hiding, it needs to be called explicitly.

class Person
{
    
    
public:
	//父类构造函数
	Person(string name = "父类")//父类有默认构造函数
		:_name(name)
	{
    
    
		cout << "Person()" << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{
    
    
		cout << "Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p)
	{
    
    
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

protected:
	string _name;
};
class Student : public Person
{
    
    
public:
	//子类构造函数
	Student(string name, int num)
		:_num(num)
		, Person(name)
	{
    
    
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)//直接传s,通过切片拿到父类的部分
		, _num(s._num)
	{
    
    
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator = (const Student& s)
	{
    
    
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
    
    
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num;
};

int main()
{
    
    
	Student s("nza", 1);
	//Student s2(s);//拷贝构造
	Student s3("kd", 7);
	s = s3;//赋值重载
	return 0;
}

insert image description here

(4) Destructor

Like constructors, destructors cannot be inherited. When creating a derived class object, the execution order of the constructor is the same as the inheritance order, that is, the constructor of the base class is executed first, and then the constructor of the derived class is executed. When the derived class object is destroyed, the execution order of the destructor and the inheritance order On the contrary, the destructor of the derived class is executed first, and then the destructor of the base class is executed.
Add the following destructor code to the above code:

    ~Person()
	{
    
    
		cout << "~Person()" << endl;
	}
	~Student()
    {
    
    
      cout<<"~Student()" <<endl;
    }

Result:
insert image description here
The result is consistent with what I said before. The destructor of the derived class will automatically call the destructor of the base class to clean up the members of the base class after being called.

4. Complex diamond inheritance and diamond virtual inheritance

(1) Diamond inheritance

Let me talk about single inheritance first: When a subclass has only one direct parent class, this inheritance relationship is called single inheritance.
insert image description here
Multiple inheritance: When a subclass has two or more direct parent classes, the inheritance relationship is called multiple inheritance.
insert image description here
Diamond inheritance: Diamond inheritance is a special case of multiple inheritance.
insert image description here
Look at the following code:

class A
{
    
    
public:
	string _name; // 姓名
};
class B : public A
{
    
    
protected:
	int _num; //学号
};
class C : public A
{
    
    
protected:
	int _id; // 职工编号
};
class D : public B, public C
{
    
    
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
    
    

	D d;
	d._name = "peter";
	
}

Writing in the main function like this will be ambiguous, and it is impossible to know which one is being accessed. As shown in the picture:
insert image description here

At this time, changing to the following code needs to show which member of the parent class to specify to access can solve the ambiguity problem, but the data redundancy problem cannot be solved. The following code and figure:

    D d;
	d.B::_name = "nza";
	d.C::_name = "kd";

insert image description here

From the structure of the object member model above, it can be seen that diamond inheritance has data redundancy and ambiguity problems. In the object of D, there will be two members of A, which will cause ambiguity. For example, the identity of a normal person is one.
This is diamond-shaped inheritance . Since the bottom derived class inherits two base classes, and at the same time, these two base classes inherit a base class, it will cause two calls of the top base class, which will cause data redundancy. and ambiguity issues. The essence of data redundancy is that space is wasted and stored too much; ambiguity is not knowing who to visit.

(2) The principle of rhombus virtual inheritance to solve data redundancy and ambiguity

(2.1) Look at the phenomena and methods

Use virtual inheritance to solve it, add virtual before the inheritance method, and first solve the redundancy problem just now.
insert image description hereAs above, we have solved the two problems of diamond inheritance, that is, adding virtual to form virtual diamond inheritance. So what is the principle?

(2.2) Look at the principle

First look at the following code:

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;
};
int main()
{
    
    
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

Without adding virtual, you can see data redundancy in the memory:
insert image description here
adding virtual: as shown in the figure:
insert image description here

It can be seen here that A is placed at the bottom of the object composition in the D object.
This A belongs to B and C at the same time, so how do B and C find the common A? Here is a table pointed to by two pointers of B and C. These two pointers are called virtual base table pointers, and these two tables are called virtual base tables. The offset stored in the virtual base table, the following A can be found through the offset. As shown in the figure below, B finds the offset through the pointer, and adds 20 bytes to the address of A, and the same is true for C.
insert image description here
Here, virtual inheritance needs to solve data redundancy and ambiguity, and put them in a common location A, where A belongs to both B and C.

(2.3) Some questions about rhombus virtual inheritance

Why not store the address of A directly?
Because pointing them to a table can not only store offsets, but also offsets of other things, and solve other problems.
Has the required space increased? Is it a loss or a gain?
Solving data redundancy requires the cost of two pointers. If class A becomes larger, it will be 8 bytes without loss or profit. If it is 16 bytes, it will be converted to 8 bytes. If it is small, it is not a complete loss, at least the problem is solved, there is no saving or wasting too much, and the cost is always fixed at 8 bytes. The space pointed to is almost negligible. A class may have multiple objects, but they still point to a table.
If the base class has multiple members, there are multiple pointers?
No, if you add members to the base class A, you don’t need to add pointers, and there is only one for each derived class, because the compiler finds the first position through the offset, and it is counted in the order of declaration. Simply put, it is next to each other , by finding the first position, the sequence may require memory alignment or directly skip multiple bytes to find the subsequent position. As shown in the picture:
insert image description here

(2.5) Inheritance Summary

Multiple inheritance is a complex embodiment of C++ grammar, and it is also one of the defects. Because there is multiple inheritance, there is diamond-shaped inheritance. With diamond-shaped inheritance, there is diamond-shaped virtual inheritance, and the underlying implementation is very complicated. Therefore, it is generally not recommended to design multiple inheritance, let alone diamond inheritance, otherwise it will be very difficult in terms of performance and complexity.

5. Inheritance and composition

(5.1) Concept

Public inheritance is an is-a relationship. That is to say, each derived class object is a base class object, such as B inherits A, and B is an A. Composition is a has-a relationship. Suppose D composes C, with a C object inside each D object.

(5.2) Comparison of Inheritance and Composition

As shown in the picture:
insert image description here

Object Inheritance: It allows to define the implementation of the derived class based on the implementation of the base class. This kind of reuse by generating derived classes is called white-box reuse. The "white box" is relatively visual. In the inheritance mode, the internal details of the base class are visible to the subclasses. Inheritance destroys the encapsulation of the base class to a certain extent, and 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. Inheritance is like a group tour in a group tour, limited by freedom. As shown in the figure above, B can directly use the two members of A, but A’s modification protection may affect B. A high degree of coupling means a high degree of correlation.
Object Composition: An alternative to class inheritance for reuse. Complex functionality can be obtained by combining objects. Object composition requires that the objects to be combined have well-defined interfaces. This style of reuse is called black-box reuse, that is, the internal details of the combined objects are invisible. There is no strong dependency between the combined classes. Prioritizing the use of object combination is beneficial to keep each class encapsulated. The combination is like free travel in group travel. As shown in the figure above, D can directly use a member (public) of C. Indirect use of another member (by accessing func in public to indirectly access members in protected), C modification protection and private members will basically not affect D, and the degree of coupling is low, that is, the association is low .

(5.3) Conclusion

Because the smaller the mutual influence, the better, and the priority is to use object composition instead of class inheritance, which is in line with an important idea of ​​software engineering: low coupling and high cohesion.
But inheritance is also very important. Some relationships are suitable for inheritance, then use inheritance, and to achieve polymorphism, inheritance is also necessary. The relationship between classes can use inheritance, you can use combination, use combination.

Guess you like

Origin blog.csdn.net/m0_59292239/article/details/130099147