[C++ Grocery Store] National Day and Mid-Autumn Festival Special - Detailed summary of polymorphism from basic to deep

Insert image description here

Article directory

1. The concept of polymorphism

The concept of polymorphism : In layman's terms, it means multiple forms. The specific point is to complete a certain behavior. When different objects complete it, different states will be produced.

Insert image description here
For example : when buying tickets for ordinary people, it is full price; when students buy tickets, it is half price; when soldiers buy tickets, they have priority. Let’s take another example: I believe everyone has participated in Alipay’s activity of scanning red envelopes, paying, and giving rewards. Then think about why some people scanned red envelopes with a large amount of 8 or 10 yuan, while others scanned red envelopes with a large amount of 8 or 10 yuan. The amounts are both 1 cent and 5 cents. In fact, there is a polymorphic behavior behind this. Alipay will first analyze your account data. For example, if you are a new user, or you do not use Alipay frequently, etc., then you need to be encouraged to use Alipay, then the amount you scan will = random % 99; if you use Alipay frequently, If you pay with Alipay or have money in your Alipay account all year round, then there is no need to encourage you to use Alipay, then your scan code amount = random % 1. To sum up: It is the same scanning action, but different users will get different red envelopes by scanning. This is also a polymorphic behavior.

2. Definition and implementation of polymorphism

2.1 Conditions for polymorphism

Polymorphism is when class objects with different inheritance relationships call the same function, resulting in different behaviors. For example, Student inherits from Person. Person objects buy tickets at full price, and Student objects buy tickets at half price. Therefore, the premise of polymorphism is that it must be in the inheritance system. There are two conditions for polymorphism in inheritance:

  • Virtual functions must be called through a pointer or reference to the base class

  • The called function must be a virtual function, and the derived class must override the virtual function of the base class

class Person
{
    
    
public:
	virtual void BuyTicket() const//虚函数
	{
    
    
		cout << "买全价票" << endl;
	}
};

class Student : public Person
{
    
    
public:
	virtual void BuyTicket() const//虚函数
	{
    
    
		cout << "买半价票" << endl;
	}
};

void Func(const Person& people)
{
    
    
	people.BuyTicket();
}

int main()
{
    
    
	Person Jack;//普通人
	Func(Jack);

	Student Mike;//学生
	Func(Mike);

	return 0;
}

Insert image description here
Insert image description here
Tips : Polymorphic calls look at the object pointed by the base class pointer or reference. If the base class pointer or reference points to a base class object, then call the member function of the base class. If it points to a derived class object, call the derived class's member function. member functions.

class Person
{
    
    
public:
	virtual void BuyTicket() const
	{
    
    
		cout << "买全价票" << endl;
	}
};

class Student : public Person
{
    
    
public:
	virtual void BuyTicket() const
	{
    
    
		cout << "买半价票" << endl;
	}
};

void Func(const Person people)
{
    
    
	people.BuyTicket();
}

int main()
{
    
    
	Person Jack;//普通人
	Func(Jack);

	Student Mike;//学生
	Func(Mike);

	return 0;
}

Insert image description here
Tips : In the above code, Funthe formal parameter of the function becomes an ordinary base class object people. In the function body, the member function BuyTicket is called through people. At this time, because people is not a pointer or reference of the base class, the function people.BuyTicket();call The conditions for polymorphic calling are not met. At this time, no matter whether the base class object or the derived class object is passed in, the BuyTicket in the base class is called, because when the polymorphic conditions are not met, calling the member function depends on the current call. The type of object, the current people is a base class object, which means that it can only call member functions in the base class, so whether we pass the base class object Jack or the derived class object Mike, the final printed result is "Buy Full price ticket”. When passing the derived class object Mike, slicing will occur, and the base class object people will be constructed using the member variables inherited from the base class in the Mike object. If the formal parameter people of the Fun function is a pointer or reference to the base class, remove the virtual in front of the BuyTicket function in the base class. At this time, the polymorphic conditions are still not met. Regardless of whether the base class object or the derived class object is passed, the final call is BuyTicke in the base class because the type of people is the base class. Summary: The two constituent conditions of polymorphism are indispensable.

2.2 Virtual functions

Virtual function : A class member function modified by virtual is called a virtual function.

class Person
{
    
    
public:
	virtual void BuyTicket() const
	{
    
    
		cout << "买全价票" << endl;
	}
};

Small Tips : Only member functions of a class can become virtual functions, and virtual cannot be added in front of global functions.

2.3 Rewriting of virtual functions

Rewriting (overwriting) of virtual functions: There is a virtual function in the derived class that is exactly the same as the base class (that is, the return value type, function name, and parameter list of the derived class virtual function and the base class virtual function are exactly the same), which is called a subclass The virtual function overrides the virtual function of the base class.

class Person
{
    
    
public:
	virtual void BuyTicket() const//虚函数
	{
    
    
		cout << "买全价票" << endl;
	}
};

class Student : public Person
{
    
    
public:
	virtual void BuyTicket() const//虚函数
	{
    
    
		cout << "买半价票" << endl;
	}
};

void Func(const Person& people)
{
    
    
	people.BuyTicket();
}

int main()
{
    
    
	Person Jack;//普通人
	Func(Jack);

	Student Mike;//学生
	Func(Mike);

	return 0;
}

Small Tips : When overriding the base class virtual function, the virtual function of the derived class can also constitute an override without adding the virtual keyword (because after inheritance, the virtual function of the base class is inherited and remains virtual in the derived class). Function attribute), but this writing method is not very standardized and is not recommended to be used in this way.

2.4 Two exceptions to virtual function overriding

2.4.1 Covariance (the return value types of base class and derived class virtual functions are different)

When a derived class overrides a base class virtual function, the return value type is different from the base class virtual function. That is, the base class virtual function returns a pointer or reference to the object of the base class (it can also be a base class in other inheritance systems), and the derived class virtual function returns a pointer or reference to the object of the derived class (it can also be a derived class in other inheritance systems). , this is called covariance. The return value type must be a pointer or a reference at the same time. One cannot be a pointer and the other a reference.

class A 
{
    
    };

class B : public A 
{
    
    };

class Person 
{
    
    
public:
	virtual A* f() 
	{
    
     
		return new A; 
	}
};

class Student : public Person 
{
    
    
public:
	virtual B* f() 
	{
    
     
		return new B; 
	}
};

2.4.2 Rewriting of destructors (the names of destructors of base classes and derived classes are different)

If the destructor of the base class is a virtual function, as long as the destructor of the derived class is defined, regardless of whether the virtual keyword is added, it will be overridden with the destructor of the base class. Although the destructor of the base class and the derived class are not The names are different, which seems to violate the rewriting rules. In fact, it is not. It can be understood that the compiler has done special processing for the name of the destructor. After compilation, the name of the destructor is unified into destructor.

class Person 
{
    
    
public:
	virtual ~Person() 
	{
    
     
		cout << "~Person()" << endl; 
	}
};

class Student : public Person 
{
    
    
public:
	virtual ~Student() 
	{
    
     
		cout << "~Student()" << endl;
		delete[] pi;
		pi = nullptr;
	}
protect:
	int* pi = new int[10];
};

void Test()
{
    
    
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
}

int main()
{
    
    
	
	Test();
	return 0;
}

Small Tips : The reason why the compiler uniformly processes the destructors of all classes into destructors is to allow the destructors of the parent and child classes to be overridden. Only the destructor of the derived class Student overrides the destructor of Person. , the delete object in the above code can constitute polymorphism, and ensure that the destructor of the objects pointed to by p1 and p2 is correctly called. If the subclass does not override the destructor of the parent class, then delete p2;there will be a problem, and it will not be able to call the destructor of the derived class Student. Because delete is divided into two steps, first calling the destructor and then calling it operator delete, and here p2is an object of the base class Person, which delete p2will eventually become: p2->destructor + operator delete(p2). If the derived class Student does not override the destructor of the base class Person, p2->destructorit does not constitute a polymorphic call, it is an ordinary call to a member function. At this time, it will be judged based on the type of the calling object whether to call a member function in the base class or not. Call the member function in the derived class (the specific rule is that the base class object calls the member function of the base class, and the derived class object calls the member function of the derived class). Here is a pointer to the p2base class object, so p2->destructorthe call must be the base class destructor, but currently p2points to an object of derived class Student, and we want to call the destructor of derived class Student to clean up the resources in the derived class Student object. In this case, what we want isp2Whoever it points to will call its destructor. Isn't this polymorphism? So we need to make the destructor of the base class a virtual function, and then the derived class can override the virtual function, so as to meet the conditions of polymorphism. The rewriting compiler has already helped us achieve it (the compiler will handle the destructor in a unified way into a function with the same name, and the destructor has no return value and parameters, perfectly satisfying the three links), we only need to add virtual in front of the base class destructor to make the destructor a virtual function. It is recommended that in the process of writing code, for a class that may be inherited, it is best to add virtual in front of its destructor to make it a virtual function.

2.5 C++11 override and final

As can be seen from the above, C++ has strict requirements for function rewriting. However, in some cases, due to negligence, the alphabetical order of the function name may not constitute rewriting, and this error will not be reported during compilation. Yes, debugging only when the expected results are not obtained when the program is running will not be worth the gain. Therefore, C++11 provides two keywords, override and final, to help users detect whether to rewrite.

2.5.1 final: Modify the virtual function to indicate that the virtual function can no longer be overridden

final : modified virtual function, indicating that the virtual function can no longer be overridden

class Car
{
    
    
public:
	virtual void Drive() final 
	{
    
    }
};
class Benz :public Car
{
    
    
public:
	virtual void Drive() 
	{
    
     
		cout << "Benz-舒适" << endl; 
	}
};

Insert image description here
Small Tips : Virtual functions are meaningless if they cannot be overridden. Here is a knowledge point to add, what should I do if a class does not want to be inherited? Method in C++98: Make the constructor of the class private. Privateness is not visible in subclasses, and the constructor of the derived class must call the constructor of the parent class. However, this approach will cause the constructor to be unable to be called when creating an object of this class. Privateness is not visible outside the class but is visible inside the class, so at this time you can write a static member function in the class specifically to create objects. . After the introduction of the final keyword in C++11, if we do not want a class to be inherited, we can modify it by adding the final keyword after the class.

2.5.2 override

override : Check whether the virtual function of the derived class has overridden a virtual function of the base class. If it has not been overridden, a compilation error will be reported.

class Car
{
    
    
public:
	virtual void Drive() 
	{
    
    }
};
class Benz :public Car
{
    
    
public:
	virtual void Drive() override
	{
    
     
		cout << "Benz-舒适" << endl; 
	}
};

3. Comparison of overloading, hiding (redefinition), and overwriting (rewriting)

Insert image description here

4. The principle of polymorphism

4.1 Virtual function table

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
    
    
	
	cout << sizeof(Base) << endl;
	return 0;
}

Insert image description here
Insert image description here
Tips : Through the above printing results and debugging, we found that a Base object is 8 bytes. In addition to the _bmembers, there is also one more one _vfptrplaced in front of the object member variables (note that some platforms may put it at the end of the object. This is the same as It depends on the platform). _vfptrIt is essentially a pointer, which we call a virtual function table pointer (v stands for virtual, f stands for function). A class containing virtual functions has at least one virtual function table pointer, because the address of the virtual function must be placed in the virtual function table (virtual functions essentially exist in code segments), and the virtual function table is also referred to as the virtual table.

4.2 Virtual function table in derived class objects

Above we looked at a virtual table in a common class object. Now let's take a look at the virtual table in a derived class.

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
    
    
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
    
    
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
    
    
public:
	virtual void Func1()
	{
    
    }
private:
	int _d = 2;
};
int main()
{
    
    
	Base b;
	Derive d;
	return 0;
}

Insert image description here
Through the monitoring window we discovered the following problems:

  • There is also a virtual table in the derived class object d, but this virtual table is inherited as part of the base class member. In general, the d object consists of two parts. One part is the member inherited from the parent class. The virtual table pointer in the d object is one of these members. The other part is its own members.

  • The virtual tables of the base class b object and the derived class d object are different. The above code has Func1completed the rewriting, so the virtual table of d stores the rewritten one Derive::Func1, so the rewriting of the virtual function is also called overwriting. Coverage refers to the coverage of virtual functions in the virtual table. Rewriting is called grammatical level, and coverage is called principle level.

  • In addition, Func2it is a virtual function after inheritance, so it is put into the virtual table. It is Func3also inherited, but it is not a virtual function, so it is not put into the virtual table.

  • The virtual function table is essentially an array of function pointers that stores virtual function addresses. Generally, a nullptr is placed at the end of this array.

  • To summarize the generation of derived class virtual tables:

    1. First copy the contents of the virtual table in the base class to the derived class virtual table.

    2. If a derived class overrides a virtual function in the base class, use the derived class's own virtual function to overwrite the virtual function of the base class in the virtual table.

    3. The newly added virtual functions of the derived class are added to the end of the derived class's virtual table in the order of declaration in the derived class. (It is not visible in the virtual table displayed in the VS monitoring window. The following will show you how to verify it through the program)

  • There is another confusing question here: where do virtual functions exist? Where does the virtual table exist? Many friends will think: virtual functions exist in virtual tables, and virtual tables exist in objects. Please note that this answer is wrong. It is emphasized again: the virtual table stores the address of the virtual function, not the virtual function. The virtual function, like the ordinary member function, exists in the code segment, but its address is stored in the virtual table. In addition, what is stored in the object is not the virtual table, but the address of the virtual table. So where does the virtual table exist? Through verification, there is a code segment in the virtual table under VS. You can verify it yourself under Linux g++.

  • In the same program, objects of the same type share a virtual table.

4.2.1 Write a program to access the virtual function table

As mentioned above, the newly added virtual functions of the derived class are added to the end of the derived class's virtual table in the order in which they are declared in the derived class. But it cannot be seen in the monitoring window of VS. Take the following code as an example:

class Person
{
    
    
public:
	virtual void func1() const
	{
    
    
		cout << "virtual void Person::fun1()" << endl;
	}

	virtual void func2() const
	{
    
    
		cout << "virtual void Person::fun2()" << endl;
	}

	virtual void func3() const
	{
    
    
		cout << "virtual void Person::fun3()" << endl;
	}
	//protected:
	int _a = 1;
};

class Student : public Person
{
    
    
public:
	virtual void func1() const
	{
    
    
		cout << "virtual void Student::fun1()" << endl;
	}

	virtual void func3() const
	{
    
    
		cout << "virtual void Student::fun3()" << endl;
	}

	virtual void func4() const
	{
    
    
		cout << "virtual void Student::fun4()" << endl;
	}
	//protected:
	int _b = 2;
};

int main()
{
    
    
	Person Mike;

	Student Jack;
}

Insert image description here
Insert image description here
Small Tips : The virtual function table of the derived class object displayed in the monitoring window does not contain the derived class's own virtual function func4. But we can see the fourth address from the memory window. We can boldly guess that this is the address of the derived class's own virtual function func4, but we can't prove it. Let's write a piece of code to verify our guess.

typedef void (*VFPTR) ();//VFPTR是一个函数指针

//vf是一个函数指针数组,vf就是指向虚表
//虚表本质上就是一个函数指针数组
void PrintVfptr(VFPTR* vf)
{
    
    
	for (int i = 0; vf[i] != nullptr; i++)
	{
    
    
		printf("vfptr[%d]:%p----->", i, vf[i]);

		VFPTR f = vf[i];//函数指针和函数名是一样的,可以去调用该函数

		f();
	}

	printf("\n");
}

int main()
{
    
    
	Person Mike;
	int vfp1 = *(int*)&Mike;
	PrintVfptr((VFPTR*)vfp1);

	Student Jack;
	int vfp2 = *(int*)&Jack;
	PrintVfptr((VFPTR*)vfp2);
	return 0;
}

Insert image description here
Tips : From the picture above, you can see that the address printed by our program is the same as the address displayed in the monitoring window, and the virtual function func4 in the derived class was successfully called. The results shown in the picture above perfectly verify our guess. This also illustrates a problem. There is a bug in the monitoring window of VS. In the future, we cannot completely trust what the monitoring window shows us when debugging the code. We should trust what the memory window shows us more than the monitoring window. . This also reflects a problem. As long as we can get the address of the function, we can call the function. Under normal circumstances, we can only call the virtual function func4 through the derived class object. Here we directly get the address of the function. Calling, the problem here is that the function's hidden formal parameter this pointer cannot receive actual parameters, because it is not a derived class object to call the function. If member variables are accessed in the function, then there will be problems with our calling method.

4.2.2 Verification of virtual table storage location

//虚表存储位置的验证

class Person
{
    
    
public:
	virtual void func1() const
	{
    
    
		cout << "virtual void Person::fun1()" << endl;
	}
//protected:
	int _a = 1;
};

class Student : public Person
{
    
    
public:
	virtual void func1() const
	{
    
    
		cout << "virtual void Student::fun1()" << endl;
	}
//protected:
	int _b = 2;
};

int main()
{
    
    
	Person Mike;

	Student Jack;
	
	//栈区
	int a = 10;
	printf("栈区:%p\n", &a);

	//堆区
	int* pa = new int(9);
	printf("堆区:%p\n", pa);

	//静态区(数据段)
	static int sa = 8;
	printf("静态区(数据段):%p\n", &sa);

	//常量区(代码段)
	const char* pc = "hello word!";
	printf("常量区(代码段):%p\n", pc);

	//虚表
	printf("基类的虚表:%p\n", (void*)*(int*)&Mike);
	printf("派生类的虚表:%p\n", (void*)*(int*)&Jack);
}

Insert image description here
Tips : The above method of obtaining the address of the virtual table is achieved through forced type conversion. Through the monitoring window above, we can see that the address of the virtual table is always stored in the first four bytes of the object, so here we first obtain the address of the object. address, and then cast it to int* type. Why cast it to int*? Because the size of an int type is four bytes, and the type of the pointer determines the size of the memory space that the pointer can access. An int* pointer can access four bytes, and then dereference the int*. In this way, the first four bytes of data in the memory space can be accessed, and the address of the virtual table can be accessed. By printing the results, we can see that the address of the virtual table is closest to the address of the constant area (code segment), so we can boldly guess that the virtual table is stored in the constant area (code segment).

4.3 Principles of polymorphism

Having said so much above, what exactly is the principle of polymorphism?

Insert image description here
Insert image description here

Tips : Let’s analyze the above picture again. When people points to the base class object Jack, the people.BuyTicket()virtual function found in Jack’s virtual table is Person::BuyTicket(); when people points to the derived class object Mike, the people.BuyTicket()virtual function found in Mike’s virtual table is Virtual functions are Student::BuyTicket(). In this way, different objects can show different forms when completing the same behavior. Secondly, through the analysis of the assembly code, it can be found that the function calls that satisfy polymorphism are not determined at compile time, but are fetched from the object after running. Function calls that do not satisfy polymorphism are determined at compile time.

Insert image description here

Insert image description here
Small Tips : It can be seen from the above two pictures that when polymorphism is met, no matter whether a base class object or a derived class object is passed, the final conversion into assembly code will be the same. The final function call is fetched from the object after the code is run.

Insert image description here
Small Tips : Ordinary function calls are determined at compile time, just call that function. The call function is related to the type of the object that calls the function. The object called BuyTicket here is a Person type, which determines that the BuyTicket function called must be in the base class.

4.3.1 Why can’t it be a pointer or reference to a derived class?

Answer : Because only base class pointers and references can point to both base class objects and derived class objects. A pointer or reference of a derived class can only point to a derived class object, not a base class object.

4.3.2 Why can’t it be an object of the parent class?

Answer : Because if it is a parent class object, assuming it is A, then when a derived class object is assigned to the parent class object A, slicing will occur, and the values ​​of the member variables of the parent class in the derived class will be used to initialize the object. Parent class object A, but the virtual table in the derived class object will not be copied to the parent class object, so whether a base class object is assigned to base class object A, or a derived class object is assigned to base class object A , the virtual table in the base class object A is always the base class's own, and what is called is always the base class's own virtual function. It is impossible to pass the base class to call the base class's virtual function, and pass the derived class to call the derived class's virtual function. With virtual functions, polymorphism cannot be implemented. The reason why the pointers and references of the parent class can be realized is that it is no problem for the pointers and references of the parent class object to point to a parent class object. When pointing to a derived class object, formal slicing will occur, that is, this kind of slicing does not It is not a real slice. Suppose there is a base class pointer p, which points to a derived class object. The slice here essentially limits the "field of view" of the p pointer, that is, the p pointer can only "see" the Those members of the derived class object that are inherited from the parent class do not actually recreate a base class object as before. And according to the screenshot of the monitoring window in Section 4.2, we can find that the virtual table of the derived class is essentially inherited as part of the parent class member, but the contents of the virtual table will be slightly modified (see how to modify it for details) Section 4.2), calling it the derived class's own virtual table, so when the p pointer points to a derived class object, it can call the derived class's own virtual function based on the derived class's virtual table. Only in this way can the requirements of polymorphism be met.

class Person
{
    
    
public:
	virtual void func1() const
	{
    
    
		cout << "virtual void Person::fun1()" << endl;
	}

	virtual void func2() const
	{
    
    
		cout << "virtual void Person::fun2()" << endl;
	}

	virtual void func3() const
	{
    
    
		cout << "virtual void Person::fun3()" << endl;
	}
//protected:
	int _a = 1;
};

class Student : public Person
{
    
    
public:
	virtual void func1() const
	{
    
    
		cout << "virtual void Student::fun1()" << endl;
	}

	virtual void func3() const
	{
    
    
		cout << "virtual void Student::fun3()" << endl;
	}

	virtual void func4() const
	{
    
    
		cout << "virtual void Student::fun4()" << endl;
	}
//protected:
	int _b = 2;
};

int main()
{
    
    
	Person Mike;
	
	Student Jack;
	Jack._a = 9;
	
	Mike = Jack;
}

Insert image description here
Tips : If the virtual function table is copied here, it will be messy. If it is copied, then when a base class pointer (Person*) points to Mike, the virtual function of the derived class is called, and there are Some virtual functions belong to the derived class itself, so this is outrageous. After a quick operation, a base class pointer can call a function that is not in its own class. It’s too outrageous, it’s too outrageous, you must not do this. To summarize here: I just want to tell you that the process of assigning a derived class object to a base class object will involve slicing, but the virtual table will not be copied.

4.3.3 Why should the virtual function of the parent class be rewritten in a derived class?

Answer : The virtual table in the derived class is essentially inherited from the parent class. The virtual table of the parent class will be copied first. If the virtual function of the parent class is rewritten, the copied virtual table will be rewritten. Modify and save the address of the virtual function overridden by the derived class. If the derived class does not override the virtual function of the base class, then the virtual table of the derived class stores the address of the base class virtual function copied from the base class virtual table, which defeats the original purpose of polymorphism. It doesn't make sense yet.

4.4 Dynamic binding and static binding

  • Static binding is also called early binding (early binding). It determines the behavior of the program during program compilation. It is also called static polymorphism, such as function overloading.

  • Dynamic binding, also known as late binding (late binding), determines the specific behavior of the program based on the specific type obtained during the running of the program, and calls specific functions, also known as dynamic polymorphism.

  • The screenshot of the assembly code in Section 4.3 shows very well what static (compiler) binding and dynamic (runtime) binding are.

5. Virtual function table for multiple inheritance relationships

5.1 Ordinary multiple inheritance

Above we have all explored the virtual function table in the single inheritance system. So what does the virtual function table look like in the multiple inheritance relationship? Let’s find out below. Based on the previous experience, we can no longer trust what the monitoring window shows us, so here we directly use the program to print the virtual function address in the virtual table in the memory space.

class Base1 
{
    
    
public:
	virtual void func1() 
	{
    
     
		cout << "Base1::func1" << endl; 
	}
	virtual void func2() 
	{
    
     
		cout << "Base1::func2" << endl; 
	}
private:
	int b1 = 0;
};

class Base2 
{
    
    
public:
	virtual void func1() 
	{
    
     
		cout << "Base2::func1" << endl; 
	}
	virtual void func2() 
	{
    
     
		cout << "Base2::func2" << endl; 
	}
private:
	int b2 = 2;
};

class Derive : public Base1, public Base2 
{
    
    
public:
	virtual void func1() 
	{
    
     
		cout << "Derive::func1" << endl; 
	}

	virtual void func3() 
	{
    
     
		cout << "Derive::func3" << endl; 
	}
private:
	int d1 = 3;
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vTable[])
{
    
    
	cout << "虚表地址:" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
    
    
		printf("vTable[%d]:0X%p--------->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
    
    
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);

	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

Insert image description here
Insert image description here

Tips : By printing the results, you can find that for the derived class Derive with multiple inheritance, there will be two virtual tables in its object because it inherits two classes. One of the two classes inherits from Base1, and the other A table inherited from Base2, the derived class's own virtual function address will be stored in the virtual table of the first inherited base class. In addition, there is another thing worth noting: there is a func1 function in both base classes, and their return value types, function names, and parameters are exactly the same. This func1 function has been rewritten in the derived class, and it was originally inherited. What is stored in the virtual table is the address of their own internal func1 function. After the derived class is rewritten, the addresses of the func1 function in the two virtual tables should be overwritten with the address of the func1 function in the derived class, but it can be seen by printing the results. The addresses of the func1 functions stored in the two virtual tables are different, but the same function is ultimately called. Both of them call the func1 function rewritten in the derived class. Why is this? Let me explain the reason to you through the disassembly of the following code.

int main()
{
    
    
	Derive d;

	Base1* p1 = &d;
	p1->func1();

	Base2* p2 = &d;
	p2->func1();
}

Insert image description here
Tips : Through disassembly, we can see that p1 is called directly, while p2 is multi-layered encapsulated. The main purpose of multi-layer encapsulation of p2 calls is to execute sub ecx , 8. Here ecxis a register, which stores thisthe value of the pointer. So why do we need to subtract 8 from it? Let's first look at ecxwhat value was stored in before subtracting 8.

Insert image description here
Small Tips : We can find that the value of the pointer ecxis originally stored p2, so why do we need to subtract 8 from this value? Because p2 is originally a pointer to a base class, and the hidden parameter this in the fucn1 function is a pointer to a derived class. A base class pointer cannot be assigned to a derived class pointer. In other words, a derived class pointer cannot point to a base class object. The reason is that the type of the pointer determines the content that the pointer can access. A derived class pointer should be able to All members in the derived class can be accessed. When a derived class pointer points to a base class object, since the base class object cannot have members of the derived class, the derived class pointer will fail when accessing these members. Something went wrong. The 8 here is essentially the size of a Base1 class object, so the purpose of subtracting 8 here is to store the first address of the d object in p2, so that p2 is equivalent to pointing to a derived class (Derive) object. At this time, go There is no problem in calling the func1 function. So to summarize, only one func1 function has been rewritten in Derive, and sub ecx , 8the purpose here is to correct thisthe pointer. The reason why p1 does not need to be modified is that the first address of the d object is originally stored in p1, and there is no problem in calling func1. Secondly, I would like to add that p1 and p2 here are both polymorphic calls when they call the func1 function. (The above is a solution under VS, other compilers may have different processing methods).

5.2 Diamond inheritance, diamond virtual inheritance

In practice, we do not recommend designing diamond-shaped inheritance and diamond-shaped virtual inheritance. On the one hand, it is too complex and prone to problems. On the other hand, such a model has certain performance losses when accessing base class members. Therefore, we do not need to study the virtual table of diamond inheritance and diamond virtual inheritance very clearly, because they are rarely used. If you are interested in this aspect, here are two articles I recommend to you: C++ virtual function table analysis , C++ object memory layout

5.2.1 Ordinary diamond inheritance

class A
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "A::func1()" << endl;
	}

	virtual void func2()
	{
    
    
		cout << "A::func2()" << endl;
	}
protected:
	int _a = 1;
};

class B : public A
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "B::func1()" << endl;
	}

	virtual void func3()
	{
    
    
		cout << "B::func3()" << endl;
	}
protected:
	int _b = 2;
};

class C : public A
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "C::func1()" << endl;
	}

	virtual void fun4()
	{
    
    
		cout << "C::func4()" << endl;
	}
protected:
	int _c = 3;
};

class D : public B, public C
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "D::func1()" << endl;
	}

	virtual void func3()
	{
    
    
		cout << "D::func3()" << endl;
	}

	virtual void fun5()
	{
    
    
		cout << "D::func5()" << endl;
	}
protected:
	int _d = 4;
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vTable[])
{
    
    
	cout << "虚表地址:" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
    
    
		printf("vTable[%d]:0X%p--------->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
    
    
	D d;
	B* p1 = &d;
	PrintVTable((VFPTR*)*(int*)p1);

	C* p2 = &d;
	PrintVTable((VFPTR*)*(int*)p2);
}

Insert image description here
Insert image description here
Insert image description here
Small Tips : There is no difference between the virtual table of ordinary diamond inheritance and multiple inheritance.

5.2.2 Diamond virtual inheritance

class A
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "A::func1()" << endl;
	}

	virtual void func2()
	{
    
    
		cout << "A::func2()" << endl;
	}
//protected:
	int _a = 1;
};

class B : virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "B::func1()" << endl;
	}

	virtual void func3()
	{
    
    
		cout << "B::func3()" << endl;
	}
//protected:
	int _b = 2;
};

class C : virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "C::func1()" << endl;
	}

	virtual void fun4()
	{
    
    
		cout << "C::func4()" << endl;
	}
//protected:
	int _c = 3;
};

class D : public B, public C
{
    
    
public:
	virtual void func1()
	{
    
    
		cout << "D::func1()" << endl;
	}

	virtual void func3()
	{
    
    
		cout << "D::func3()" << endl;
	}

	virtual void fun5()
	{
    
    
		cout << "D::func5()" << endl;
	}
//protected:
	int _d = 4;
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vTable[])
{
    
    
	cout << "虚表地址:" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
    
    
		printf("vTable[%d]:0X%p--------->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

void Test()
{
    
    
	D d;
	B* p1 = &d;
	PrintVTable((VFPTR*)*(int*)p1);

	C* p2 = &d;
	PrintVTable((VFPTR*)*(int*)p2);

	A* p3 = &d;
	PrintVTable((VFPTR*)*(int*)p3);
}

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

	Test();
}

Insert image description here
Insert image description here
Insert image description here
Tips : As can be seen from the printing results and the above picture, in the diamond virtual inheritance system, the members of class A are separated. They no longer have one copy in class B and class C. Therefore, in class B and class C, Class A and class D objects each have a virtual function table of class A. There is a problem here, that is, if the virtual function in class A is rewritten in class B and class C at the same time, then this virtual function must also be rewritten in class D, such as the func1 function in the above code, because if in class D If there is no rewriting, then whether the object of class D stores the one overridden in class B or the one overridden in class C is stored. At this time, there will be ambiguity, as long as this virtual function is also rewritten in class D. , there will be no ambiguity. Secondly, class D does not have its own virtual table, that is, for class D's own virtual function, the compiler will store the address of this function in the virtual table of the first class inherited by class D. Here, it is stored In a Class B virtual table.

Supplement : As mentioned in the previous article, offsets are stored in the virtual base table in order to find the separated base class members, which are class A members. However, when I saw through the memory window, this offset The offset is stored in the second byte of the virtual base table, so what is stored in the first byte of the virtual base table? The answer is: the offset is also stored. The purpose of storing this offset is to find the first address of the class in the memory. Let’s take the above code as an example, because if a class has a virtual table, then the virtual table address is It is stored at the very beginning of this class object. The offset stored in the first byte in the virtual base table is used to find the first address of the object. The offset stored in the second byte in the virtual base table is used to find the first address of the object. It is used to find the first address of the base class.

6. Abstract class

6.1 Concept

If you write after the virtual function =0, the function is a pure virtual function. A class containing pure virtual functions is called an abstract class (also called an interface). Abstract classes cannot instantiate objects. A derived class cannot instantiate an object after inheritance. Only by overriding a pure virtual function can a derived class instantiate an object. Pure virtual functions specify that derived classes must be rewritten. In addition, pure virtual functions also reflect interface inheritance.

//抽象类(接口)
class Car
{
    
    
public:
	virtual void Drive() const = 0;
};

class Benz : public Car
{
    
    
public:
	virtual void Drive() const
	{
    
    
		cout << "Benz-舒适" << endl;
	}
};

class Bmw : public Car
{
    
    
public:
	virtual void Drive() const
	{
    
    
		cout << "Bmw-操控" << endl;
	}
};

void Advantages(const Car& car)
{
    
    
	car.Drive();
}

int main()
{
    
    
	Benz be;
	Advantages(be);

	Bmw bm;
	Advantages(bm);
}

Insert image description here

6.2 Interface inheritance and implementation inheritance

Inheritance of ordinary functions is a kind of implementation inheritance. The derived class inherits the functions of the base class and can use the functions and the implementation of the inherited functions. The inheritance of virtual functions is a kind of interface inheritance. The derived class inherits the interface of the base class virtual function. The purpose is to override and achieve polymorphism. What it inherits is the interface. So if you don't implement polymorphism, don't define the function as a virtual function.

7. Polymorphic common interview questions

//下面这段代码的运行结果是什么?
class A
{
    
    
public:
    virtual void func(int val = 1) 
    {
    
     
        std::cout << "A->" << val << std::endl; 
    }
    virtual void test() 
    {
    
     
        func(); 
    }
};

class B : public A
{
    
    
public:
    void func(int val = 0) 
    {
    
     
        std::cout << "B->" << val << std::endl; 
    }
};

int main()
{
    
    
    B* p = new B;
    p->test();
    return 0;
}

Insert image description here
analyze: To answer this question correctly, we must understand the following points. The first is our understanding of inheritance. Class B inherits class A, so it is no problem for a pointer p of class B to call test. Class B inherits the test function of class A. In the test function, the func function is called again. This func function is essentially called with this pointer, and the func function is a virtual function, and the subclass rewrites it. So is it too much to call the func function here? What about stateful calls? Whether it is a polymorphic call depends on who is calling the func function here. As mentioned before, the func here is essentially called by this pointer. So what type is the this pointer here? If it is a base class (type A), then it conforms to polymorphic calling. If it is derived (type B), it does not conform to polymorphic calling. So what type is this here? This will examine everyone’s understanding of inheritance. Let me talk about the answer first. The this pointer here is of type A*. Many friends may think that if class B inherits class A, then a test function will be regenerated in class B, and then the p pointer here will call the test function generated by the bytes in class B, so here The this pointer should be of type B*, but it is not and the compiler does not do this. The derived class object model in inheritance is generated in the following way. For member variables, a derived class (here, class B) object is created, which is divided into two parts. The first part is the parent class, and the second part is Himself, he will put together the member variables inherited from the parent class as a parent class object, and then treat this object as a member variable of the derived class, so it must be called in the initialization list of the derived class constructor The constructor of the parent class must call the destructor of the parent class in the destructor of the derived class. This is the storage model of a derived class object in memory. The storage model of the object is only related to member variables and has nothing to do with member functions. All compiled functions are placed in the code segment. Since the test function is not rewritten in the derived class B, the code of the test function will not be generated twice. There is only one copy of the test function from beginning to end. , that is, generated by base class A, so the this pointer in the test function here is A*. The p pointer is calling test function, first perform a syntax check, first look for the test function in the derived class B, if it is not found, then go to the parent class A to look for it, and finally find it, there is no problem with the syntax, and then in the link stage, the test function is For the parent class, the compiler uses the name modified by the function name modification rules to find the function. Having said so much before, I just want to tell you that the this pointer here is A*, so polymorphic calling is satisfied here. This means that different types of objects will have different effects when calling the test function. When a base class object calls the test function, it will eventually call the func function in the base class. When a derived class object calls the test function, it will eventually call the func function in the derived class. func function. And here is a pointer p of a derived class to call the test function, so the func function in the derived class is ultimately called. At this time, some friends will have questions about the default value of the formal parameter val of the func function in the derived class. It's obviously 0, why is it printed as 1? Isn't 1 the default value of the func function parameter in the parent class? This involves the second "pit point" of this question: virtual function rewriting. What is rewritten is the implementation (only the function body is rewritten). This is why the rewritten virtual function in the derived class does not need to be virtual. For the rewritten virtual function, the compiler will check whether it meets the three similarities, that is, whether the return value type, function name, and parameter list are the same (the same parameter list means the same number of parameters and the same type order). As long as it complies with the three compilers, it doesn't matter. The entire shell of the virtual function rewritten in the derived class (that is, the set of function declarations) is used in the parent class. Therefore, the val used in the function body of the virtual function func rewritten in the derived class should be the default value of val in the parent class. The formal parameter list of the virtual function func rewritten in the derived class gives the default value. It makes no sense. The test function will eventually call the func function in the base class, and the derived class object will eventually call the func function in the derived class. And here is a pointer p of a derived class to call the test function, so the func function in the derived class is ultimately called. At this time, some friends will have questions about the default value of the formal parameter val of the func function in the derived class. It's obviously 0, why is it printed as 1? Isn't 1 the default value of the func function parameter in the parent class? This involves the second "pit point" of this question: virtual function rewriting. What is rewritten is the implementation (only the function body is rewritten). This is why the rewritten virtual function in the derived class does not need to be virtual. For the rewritten virtual function, the compiler will check whether it meets the three similarities, that is, whether the return value type, function name, and parameter list are the same (the same parameter list means the same number of parameters and the same type order). As long as it complies with the three compilers, it doesn't matter. The entire shell of the virtual function rewritten in the derived class (that is, the set of function declarations) is used in the parent class. Therefore, the val used in the function body of the virtual function func rewritten in the derived class should be the default value of val in the parent class. The formal parameter list of the virtual function func rewritten in the derived class gives the default value. It makes no sense. The test function will eventually call the func function in the base class, and the derived class object will eventually call the func function in the derived class. And here is a pointer p of a derived class to call the test function, so the func function in the derived class is ultimately called. At this time, some friends will have questions about the default value of the formal parameter val of the func function in the derived class. It's obviously 0, why is it printed as 1? Isn't 1 the default value of the func function parameter in the parent class? This involves the second "pit point" of this question: virtual function rewriting. What is rewritten is the implementation (only the function body is rewritten). This is why the rewritten virtual function in the derived class does not need to be virtual. For the rewritten virtual function, the compiler will check whether it meets the three similarities, that is, whether the return value type, function name, and parameter list are the same (the same parameter list means the same number of parameters and the same type order). As long as it complies with the three compilers, it doesn't matter. The entire shell of the virtual function rewritten in the derived class (that is, the set of function declarations) is used in the parent class. Therefore, the val used in the function body of the virtual function func rewritten in the derived class should be the default value of val in the parent class. The formal parameter list of the virtual function func rewritten in the derived class gives the default value. It makes no sense.

7.1 Quick questions and answers

Can inline functions be virtual functions?
Answer : Yes, but the compiler will ignore the inline attribute, and this function is no longer inline, because the virtual function must be placed in the virtual function table.

Can static members be virtual functions?
Answer : No, because the static member function does not have this pointer, and the virtual function table cannot be accessed using the calling method of type::member function, so the static member function cannot be put into the virtual function table.

Can the constructor be a virtual function?
Answer : No, because the virtual function table pointer in the object is initialized during the constructor initialization list stage.

Can the destructor be a virtual function? In what scenarios is the destructor a virtual function?
Answer : Yes, and it is best to define the destructor of the base class as a virtual function. For collective scenes, refer to Section 2.4.2.

Is it faster for objects to access ordinary function blocks or virtual functions?
Answer : First of all, if it is a normal object, it is just as fast. If it is a pointer object or a reference object, the ordinary function called is faster. Because it constitutes polymorphism, calling a virtual function at runtime requires searching in the virtual function table.

At what stage is the virtual function table generated? Where does it exist?
Answer : The virtual function table is generated during the compilation phase, and generally there is a code segment (constant area).

What is an abstract class? What is the role of abstract classes?
Answer : Please refer to Section 6.1 for what an abstract class is. Abstract classes force overriding of virtual functions, and abstract classes reflect interface inheritance relationships.

8. Conclusion

Today’s sharing ends here! If you think the article is good, you can support it three times . There are many interesting articles on Chunren's homepage . Friends are welcome to comment. Your support is the driving force for Chunren to move forward!

Insert image description here

Guess you like

Origin blog.csdn.net/weixin_63115236/article/details/133108573
Recommended