Analysis of the core key knowledge of C + + inheritance and polymorphism

I inherit

  • The essence of inheritance is to reuse the data members and methods of the base class  
  • The essence of encapsulation is to expose only the necessary use interfaces to the outside world. The internal specific implementation details and some core interfaces are invisible to the outside world, hide the details, and only open the necessary functional interfaces to the outside world
  • It is because of the need to encapsulate and hide that public and private attributes are generated Public is visible outside the class and inside the class. Private is only exposed to our program developers and designers. Users are invisible, that is, invisible outside the class

1.1. Three ways of graphic inheritance

  • If it's school teaching, generally speaking, the beginning is to remember the following picture, but it's OK after understanding it

  • Note 1: the private data members of the base class are absolutely invisible in the derived class. Imagine that it is understandable that your father's private affairs can't tell you, so the private members of the class are only visible in this class and invisible in the derived class
  • Note 2: what is the purpose of the protection attribute?? Is to be visible in the derived class and not outside the class. Imagine if you need some data members that are inaccessible to users outside the class, but visible to your derived classes. At this time, the private attribute must not be achieved, so protected came into being Therefore, for classes that need to be inherited, we often use protected to define member objects instead of private  
  • Inheritance method: public > protected > private , the inherited member attributes. The public inherited attributes remain unchanged. Protected , and , private , inheritance upgrade the corresponding attributes with less restrictions to attributes with greater restrictions
  • Default inheritance method: class is private inheritance, struct is public inheritance
  • Using inheritance, our inheritance method is generally public inheritance, because other inheritance methods can't reuse code

1.2. Inherited memory distribution map. Private member subclasses of the parent class are not visible, but do you want to inherit it??

  • Conclusion: the subclass will inherit all the members of the parent class. It is not visible, but only a syntactic restriction. In fact, there is this variable in memory, but only a syntactic restriction on access
class Base {
public:
	int a;
private:
	int b;
};

class Derived : public Base {
};
int main() {
	cout << sizeof(Base) << endl;   //??
	cout << sizeof(Derived) << endl;//??
	return 0;
}

  • It turns out that it is all inherited. Continue to look at the memory
class Base {
public:
	Base(int a, int b) 
		: _a(a)
		, _b(b) 
	{}
	int _a;
private:
	int _b;
};
class Derived : public Base {
public:
	Derived(int a = 0, int b = 0, int c = 0) 
		: Base(a, b)
		, _c(c) 
	{}
private:
	int _c;
};
int main() {
	Derived d(1, 2, 3);
	return 0;
}

It can be seen that although private members are not visible in derived classes, they also exist in essence, only syntactically restricting access     

1.3. Assignment between parent and child objects

Cutting:

Slicing: slicing refers to the data method that restricts access to the base class of the slice. It is different from cutting. It will not be copied again, but directly point to the corresponding base class, and then restrict access to members of the data base class. This is the essence of slicing reference

  • Make good use of various monitoring windows and disassembly windows of the compiler, which is particularly good for our study and analysis

1.4. Inheritance and static members

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

Static static members are specific to a class. No matter how many subclasses are derived, they are just an instance object of static members. They will not produce one copy in a derived class, but a global unique copy:

class Base {
public:
	Base(int a, int b) 
		: _a(a)
		, _b(b) 
	{}
	int _a;
private:
	int _b;
	static int count;
};
int Base::count = 0;
class Derived1 : public Base {
public:
	Derived1(int a = 0, int b = 0, int c = 0) 
		: Base(a, b)
		, _c(c) 
	{}
private:
	int _c;
};
class Derived2 : public Base {
public:
	Derived2(int a = 0, int b = 0, int c = 0)
		: Base(a, b)
		, _c(c)
	{}
private:
	int _c;
};
int main() {
	Derived1 d1(1, 2, 3);
	Derived2 d2(1, 2, 3);
	return 0;
}

The process data members of Base class will not exist in the derived class objects. They belong to the whole Base class and are defined before entering the main function. They are globally unique and stored in the static data segment

1.5. Diamond inheritance caused by multiple inheritance (redundancy of data and ambiguity of data access)

  • This inheritance method is called diamond inheritance
class A {
public:
	A(int a = 0)
		: _a(a) {
	}
	int _a;
};

class B : public A {
public:
	B(int a = 0, int  b = 0) 
		: A(a)
		, _b(b) {
	}
	int _b;
};

class C : public A {
public:
	C(int a = 0, int c = 0) 
		: A(a)
		, _c(c) {
	}
	int _c;
};

class D : public B, public C {
public:
	D(int a = 0, int b = 0, int c = 0, int d = 0) 
		: B(a, b)
		, C(a, c)
		, _d(d) {
	}
	int _d;
};

Direct access to the above_ A member has a problem because he doesn't know which one to visit_ a. Take a look at the memory window first:

Question 1: there are two_ a data member, causing two problems, 1 Data redundancy, (two copies) Ambiguous question (which one was visited after the interview)

Solution 1: it can only solve the problem of ambiguity, which can be accessed by specifying the access method through:

Although the above method solves the ambiguous problem of data access, it still does not solve the problem of data redundancy. In fact, the problem of data redundancy is quite large. Now the size of class A is not large, and the problem can not be displayed. If class A is particularly large, a duplicate of various data members of class a will be inherited, The storage cost of this data redundancy is still quite large

  • Solution 2: adopt virtual inheritance to deal with the above problems So that there is only one data member of class A
  • But if there is only one member of class A, when we create the object of D, will the member of ancestral class be placed in class B or class C????

  • Virtual keyword is added to modify virtual inheritance
class A {
public:
	A(int a = 0)
		: _a(a) {
	}
	int _a;
};

class B : virtual public A {
public:
	B(int a = 0, int  b = 0) 
		: A(a)
		, _b(b) {
	}
	int _b;
};

class C : virtual public A {
public:
	C(int a = 0, int c = 0) 
		: A(a)
		, _c(c) {
	}
	int _c;
};

class D : public B, public C{
public:
	D(int a = 0, int b = 0, int c = 0, int d = 0) 
		: B(a, b)
		, C(a, c)
		, _d(d) {
	}
	int _d;
};

First of all, let's observe this ordinary monitoring window, which gives us the feeling that there are two_ a among them? Is it? The answer is definitely not. It's useless to use virtual inheritance. It hasn't solved the problem of data redundancy (two ancestral data members)

The answer is that the monitor has been processed to make us look simple and don't want so much. There are details. We can really go deep into the memory window to check, Set the column to 4 columns, and the unit is an int of 4 bytes

What is stored is the address of the address gap. Through this address, you can find the location away from the data member belonging to A for access

Summing up: multi inheritance is A fucking pit father. The pit of diamond inheritance can only be filled by A virtual inheritance: data redundancy + data access ambiguity, but diamond inheritance makes the memory model of the inherited grandson derived class complex, which is not too much, The memory distribution model is no longer as clear and simple as cutting and slicing, but it is simply not too complex. It needs to store the address deviation from class A data members. In order to protect this deviation, it has not been stored directly, but it is realized by storing the address value of the difference. The problem is solved, but the learning cost is doubled and not too much

Therefore, when it is really used, it is not necessary to use single inheritance rather than multiple inheritance

1.6. Summary: inheritance, combination or inheritance?

  • Selection of inheritance and combination? Respectively:
  • Inheritance is more transparent. Inheritance is more transparent to the members using the base class in the derived class. Not only the members of the public attribute but also the members of the protected attribute are visible. In general, inheritance uses the protected attribute, so the reuse of inheritance is relatively transparent in the derived class
  • Composition is a kind of black box interface reuse, which is exactly the same as the effect outside the class. You can only see what the designer wants you to see, that is, you can only use the public interface function (generally speaking), which is very inconvenient sometimes. Sometimes, many data members cannot access it directly, so it is not very convenient to use it to further design the interface
  • There is no fixed statement on whether to use inheritance or combination. Some recommend using combination as much as possible for better encapsulation and low coupling, but I don't think there is an absolute standard, Generally, the relationship with the same basic level, such as people: black, white, yellow, is based on inheritance (if the two classes have derivative relationship, inheritance is used) of relationship. When the two classes are managed, they can be combined into a whole, and they can be combined (for example, the date class can be combined into the date class)

II polymorphic

2.1. What is polymorphism?

  • Simple understanding of polymorphism: for the same method, passing in different object calls will produce different results
  • The above is also called dynamic polymorphism. Let's start with a short section of function + result description:
class Father {
public:
	virtual void sleep() {
		cout << "I'm an adult, sleeping without bedding" << endl;
	}
};
class Son : public Father {
public:
	virtual void sleep() {
		cout << "I'm a child who sleeps" << endl;
	}
};
void Sleep(Father& who) {
	who.sleep();
}
int main() {
	Father f;
	Son s;
	Sleep(s);//What is the result of the introduction of children? 
	Sleep(f);//What is the result of incoming Fa?
	return 0;
}

  • There is also a polymorphism called static, which is determined at compile time, function overloading   

2.2. What are the necessary conditions for polymorphism?

  • Use base class pointers or references to call virtual functions
  • Override the called virtual function in a derived class

2.3. The essential condition of virtual function rewriting. The essence of rewriting is a kind of covering

  • To constitute virtual function rewriting, the derived class needs to have the same virtual function as the base class. This virtual function not only has the same function name, but also needs the same parameter return value to constitute virtual function rewriting (covering). It is said that the subclass rewrites the virtual function of the parent class
  • Special case of virtual function rewriting 1 Covariant # 2, virtual destructor override
  • Covariance: when composing virtual function rewriting, there is also a special case with different return values, which is called covariance. The virtual function of the base class returns the reference or pointer of the base class, and the virtual function of the derived class returns the reference or pointer of a derived class object
class Base {
};//Write an empty base class to return
class Derived : public Base {
};//Write an empty derived class to return

class Fa {
public:
	virtual Base* GetBase() {
		cout << "Pass in base class object, Virtual function of base class" << endl;
		cout << "The base class virtual function returns the base class pointer reference. Derived class virtual function returns derived class pointer reference, Called covariance" << endl;
		return new Base;
	}
};
class Son : public Fa {
public:
	virtual Derived* GetDerive() {
		cout << "Incoming derived class object, Remove the virtual function overridden by the derived class" << endl;
		cout << "The base class virtual function returns the base class pointer reference. Derived class virtual function returns derived class pointer reference, Called covariance" << endl;
		return new Derived;
	}
};
void test(Fa& who) {
	delete who.GetBase();
}
int main() {
	Fa f;
	Son s;
	test(f);
	test(s);
	return 0;
}

Special case 2: in fact, it is not special, because the underlying compiler will process:

Try to define destructors as virtual functions. Why?? Constructors cannot be defined as virtual functions. Why can destructors? If the function names are not the same, how to constitute the requirements of virtual function rewriting??? (try to define the destructor as a virtual destructor to form a polymorphism?)

  • Because for the destructor, the bottom compiler has done a lot of processing. The bottom compiler will process all the destructor function names into destructor as the function name of the destructor
  • Then let's talk about the call of the destructor. The construction is to call the parent class construction first, and then the child class construction. Naturally, the destructor calls the child class destructor first, and then the parent class destructor. Because we can't display the call of the destructor, we specify to call the child class destructor first and then the parent class destructor, so the call of the destructor is automatically called by the compiler
class Fa {
public:
	Fa() :_a(new int(0)) {
		cout << "constructor Fa" << endl;
	}
	~Fa() {
		delete _a;
		cout << "destructor Fa" << endl;
	}
private:
	int* _a;
};
class Son :public Fa {
public:
	Son() {
		cout << "constructor Son" << endl;
	}
	 ~Son() {
		cout << "destructor Son" << endl;
	}
};
int main() {
	Fa* f1 = new Fa;
	Fa* f2 = new Son;
	delete f1;
	delete f2;
	return 0;
}

  • How to solve the above problems? The parent class pointer points to the child class object However, the destructor of a subclass object cannot be called to free resources
  • Solution: using the characteristics of polymorphism, the destructor is defined as a virtual function?? But the function name is different? How to satisfy the virtual function rewriting condition? The compiler processes the bottom layer into destructor unified function name Add virtual and then look at the results:

2.4. Abstract class -- -- > pure virtual function

  • Abstract classes (interface classes) classes containing pure virtual functions are called abstract classes (alias interface classes)
  • Classes containing pure virtual functions are abstract classes and cannot instantiate objects. If you need to instantiate objects, you must rewrite all pure virtual functions
class Base {
public:
	virtual void Work() = 0; 
	virtual void Sleep() = 0;
};

class Derived : public Base {
public:
	virtual void Work() {
		cout << "Complete the above interface functions" << endl;
		cout << "I'm working" << endl;
	}
	virtual void Sleep() {
		cout << "Complete the above interface functions" << endl;
		cout << "I'm sleeping" << endl;
	}
};

2.5. Virtual function table... (virtual table pointer, thorough)

  • Virtual table pointer of a classic written test question

The answer to the above question is 4? Why Even if there is a placeholder, it should be 1 byte. Remove the virtual check?

  • It's not particularly strange if it's 1. This byte is occupied
  • Where did the four bytes come from?

  • The above pointer is called virtual table pointer, pointing to a virtual function table
  • All stored in this virtual function table are function entries, (function pointers) essential function pointer arrays
  • This virtual table pointer points to the above virtual function table (virtual table for short) The essence of this pointer is a three-level pointer (pointing to an array of function pointers)
typedef void (*PFunc)(); 
//PFunc is a type, function pointer type
class Base {
public:
	virtual void func1() {
		cout << "Base virtual void func1()" << endl;
	}
	virtual void func2() {
		cout << "Base virtual void func2()" << endl;
	}
	virtual void func3() {
		cout << "Base virtual void func3()" << endl;
	}
};
class Derived : public Base {
public:
	virtual void func1() {
		cout << "Derived virtual void func1()" << endl;
	}
	virtual void func2() {
		cout << "Derived virtual void func2()" << endl;
	}
	virtual void func3() {
		cout << "Derived virtual void func3()" << endl;
	}
};
//Print virtual table function
void PrintVTable(PFunc** vptr) {
	cout << "The virtual table function is called as follows: " << endl;
	PFunc* VFunArr = (PFunc*)vptr;//Cast to
	for (int i = 0; VFunArr[i] != NULL; ++i) {
		VFunArr[i]();	//Call function
	}
	cout << endl;
}
int main() {
	Derived d;
	PrintVTable((PFunc**)*(int*)&d);
	return 0;
}

  • Through the above methods, the virtual table function can be called, and VFunArr[i] () can call the virtual function. It is further proved that VFunArr[i] is the entry of the virtual function and the function pointer. It also shows that the virtual table is an array storing the virtual function pointer Function pointer () can realize function call
  • Emphasis: the virtual table stores not the virtual function, but the address, function entry and virtual function pointer of the virtual function

Draw another picture to impress you:

 

  • When it came to the disgusting part, I was in complete agreement with everyone at that time (when I first studied) The main reason why this pointer needs to be converted from the past is that the base type level of the pointer is different, and the address value jumped after the pointer + + is uncertain

III summary

  • This article from inheritance analysis to polymorphism: many small details are not introduced, mainly to explain the key and difficult points I think
  • Inheritance: it is mainly diamond inheritance caused by multiple inheritance. The processing is virtual inheritance. There is an initial base class data in the whole world. Therefore, it is inappropriate to place this data in B and C, and then how can you access this data if it is not placed in B and C? In order to access this data, the measure taken is to record the offset of this global data. In order to protect this offset, the address of this offset is stored in the object memory model
  • final keyword modifies a class that cannot inherit
  • Polymorphic interpretation: different objects call the same function and perform the same behavior will produce different results
  • Polymorphic necessary conditions: the parent class pointer points to the subclass object or refers to the subclass object, overrides the virtual function of the base class in the derived class, and calls the virtual function with the pointer or reference of the parent class
  • Special cases of polymorphism: covariance and {virtual destructor (the bottom processing is unified into destructor destructor name)
  • Why is the underlying principle of polymorphism? The essence of virtual function rewriting is coverage. It is willing to store the address of virtual function in the virtual table. Rewriting virtual function is actually an overlay operation on the virtual function in the corresponding virtual function address in the virtual table
  • How to find the virtual table? A virtual table pointer will be stored in the object model to help us find the virtual function

Keywords: C++ Back-end

Added by PRodgers4284 on Wed, 02 Mar 2022 16:15:06 +0200