[C + + from bronze to king] Chapter 18: polymorphism of C + +

Posted by xstevey_bx on Sat, 15 Jan 2022 07:44:01 +0100

Catalogue of series articles

preface

It should be noted that the code and explanation in this section are in the x86 program under vs2013, and the pointers involved are 4bytes. If you want to use other platforms, some codes need to be changed. For example, if it is an x64 program, you need to consider the problem that the pointer is 8bytes, etc.

1, Concept of polymorphism

1. The concept of polymorphism

The concept of polymorphism: Generally speaking, it is a variety of forms. Specifically, it is to complete a certain behavior. When different objects complete it, they will produce different states.

Take chestnuts for example: when ordinary people buy tickets, they buy tickets at full price; When students buy tickets, they buy tickets at half price; When soldiers buy tickets, they have priority.

Another Chestnut: Recently, in order to fight for the online payment market, Alipay will often do attractive red envelopes payment - reward activities at the end of the year. So let's think about why some people sweep big and fresh red envelopes of $8 and $10... While others sweep red envelopes of $1 and $5. In fact, behind this is also a polymorphic behavior. Alipay will first analyze your account data, such as you are new users, such as you do not often Alipay payment, etc., then you need to be encouraged to use Alipay, then you scan code amount = random()%99; For example, you often use Alipay payment or Alipay account for money all the year round, so you don't need to encourage you to use Alipay too much. Then you scan code amount = random()%1; To sum up: the same code scanning action, different users get different red envelopes, which is also a polymorphic behavior. ps: the problem of Alipay's red envelopes is a mere fabrication.

2, Definition and implementation of polymorphism

1. Composition conditions of polymorphism

Polymorphism is that class objects with different inheritance relationships call the same function, resulting in different behaviors. For example, Student inherits Person. The Person object buys tickets at full price, and the Student object buys tickets at half price.

  • Then there are two conditions for polymorphism in inheritance:
  • The virtual function 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.
#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
	}
};
class Student :public Person
{
	virtual void BuyTicket()  //The virtual keyword of a derived class can also be rewritten without writing
	{
		cout << "Buy a ticket-50% Off" << endl;
	}
};
void Func1(Person& p)
{
	p.BuyTicket();
}
void Func2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student s;

	//Transmission object
	Func1(p);
	Func1(s);

	//Address of transmission object
	Func2(&p); 
	Func2(&s);
	return 0;
}

2. Understanding of virtual function

Virtual function: that is, the class member function modified by virtual is called virtual function.

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
	}
};

3. Rewriting of virtual functions

Rewrite (overwrite) of virtual function: 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 virtual function and the base virtual function are exactly the same). It is said that the virtual function of the subclass overrides the virtual function of the base class. Note: when rewriting the virtual function of the base class, the virtual function of the derived class can also constitute rewriting without adding the virtual keyword (because the virtual function of the base class is inherited after inheritance, and the virtual function attribute is still maintained in the derived class), but this writing method is not very standardized and is not recommended.

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
	}
};
class Student :public Person
{
	virtual void BuyTicket()  //The virtual keyword of a derived class can also be rewritten without writing
	{
		cout << "Buy a ticket-50% Off" << endl;
	}
};
void Func1(Person& p)
{
	p.BuyTicket();
}
void Func2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student s;

	//Transmission object
	Func1(p);
	Func1(s);

	//Address of transmission object
	Func2(&p); 
	Func2(&s);
	return 0;
}

1. Covariance of two exceptions to virtual function rewriting

  • Covariance (the return value type of virtual function of base class and derived class is different).
  • When a derived class overrides a base class virtual function, the return value type is different from that of the base class virtual function. That is, when the base class virtual function returns the pointer or reference of the base class object, and the derived class virtual function returns the pointer or reference of the derived class object, it is called covariance.

Pointer to object of return value type:

class Person
{
public:
	virtual Person* BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
		return 0;
	}
};
class Student :public Person
{
	virtual Student* BuyTicket()  //The virtual keyword of a derived class can also be rewritten without writing
	{
		cout << "Buy a ticket-50% Off" << endl;
		return 0;
	}
};
void Func1(Person& p)
{
	p.BuyTicket();
}
void Func2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student s;

	//Address of transmission object
	Func2(&p);
	Func2(&s);
	return 0;
}


Reference with return value type of object:

class Person
{
public:
	virtual Person& BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
		return *this;
	}
};
class Student :public Person
{
	virtual Student& BuyTicket()  //The virtual keyword of a derived class can also be rewritten without writing
	{
		cout << "Buy a ticket-50% Off" << endl;
		return *this;
	}
};
void Func1(Person& p)
{
	p.BuyTicket();
}
void Func2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student s;

	//Transmission object
	Func1(p);
	Func1(s);

	//Address of transmission object
	Func2(&p);
	Func2(&s);
	return 0;
}

2. The two exceptions to virtual function rewriting are the rewriting of destructors

If the destructor of the base class is a virtual function, the derived class destructor, as long as it is defined, whether or not the virtual keyword is added, constitutes an override with the destructor of the base class, although the name of the destructor of the base class is different from that of the derived class. Although the function names are different, it seems to violate the rewriting rules. In fact, it is not. It can be understood that the compiler has made special treatment on the name of the destructor, and the name of the destructor after compilation is uniformly processed into destructor.

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student :public Person
{
public:
	virtual ~Student()   //Rewriting without virtual
	{
		cout << "~Student()" << endl;
	}
};
// Only when the destructor of the derived class Student overrides the destructor of Person, can the following delete object call the destructor to form polymorphism and ensure that the objects pointed to by p1 and p2 call the destructor correctly.
int main()
{
	Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;
	delete p2;
	return 0;
}

4.C++11 override and final

It can be seen from the above that C + + has strict requirements for function rewriting, but in some cases, due to negligence, it may cause the parent order of function names to be written, but it will not form an overload, and this error will not be reported during compilation. Debugging will not pay off until the program runs without the expected results, Therefore, C++11 provides two keywords: override and final, which can help users detect whether to rewrite.

1.final: modifies a virtual function, indicating that the virtual function can no longer be rewritten

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Car
{
public:
	virtual void Drive() final
	{}
};
class Benz :public Car
{
	virtual void Drive()
	{
		cout << "Benz-comfortable" << endl;
	}
};
int main()
{
	Car c;
	Benz b;
	c.Drive();
	b.Drive();
	return 0;
}

2.override: check whether the virtual function of the derived class overrides a virtual function of the base class. If not, compile and report an error

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Car
{
public:
	virtual void Drive()
	{}
};
class Benz :public Car
{
public:
	virtual void Dirve() override
	{
		cout << "Benz-comfortable" << endl;
	}
};
int main()
{
	Car c;
	Benz b;
	c.Drive();
	b.Drive();
	return 0;
}

5. Comparison of overloading, overwriting (Rewriting) and hiding (redefining)

3, Understanding of abstract classes

1. Concept of abstract class

Write = 0 after the virtual function, then the function is a pure virtual function. Classes containing pure virtual functions are called abstract classes (also called interface classes). Abstract classes cannot instantiate objects. A derived class cannot instantiate an object after inheritance. A derived class can instantiate an object only by overriding a pure virtual function. Pure virtual functions regulate that derived classes must be overridden. In addition, pure virtual functions embody interface inheritance. Pure virtual functions also require subclasses to be overridden.

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-comfortable" << endl;
	}
};
class BMW:public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-operation" << endl;
	}
};
int main()
{
	Car* p = new Benz;
	p->Drive();

	Car* b = new BMW;
	b->Drive();

	Car c;

	Benz b;

	BMW bb;
	return 0;
}

2. Interface inheritance and implementation inheritance

  • The inheritance of ordinary functions is an implementation inheritance. Derived classes inherit the functions of the base class. Functions can be used. What they inherit is the implementation of functions.
  • The inheritance of virtual function is an interface inheritance. The derived class inherits the interface of the base class virtual function. The purpose is to rewrite and achieve polymorphism. The inherited interface is the interface. So if you don't implement polymorphism, don't define functions as virtual functions.

4, Principle of polymorphism

1. Virtual function table

Here we often take a test question: how much is sizeof(Base)?

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:

	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	return 0;
}

On win32 platform:

On x64 platform:

Through observation and test, we found that the b object is 8bytes, except_ b members, one more__ vfptr is placed in front of the object (note that some platforms may be placed at the back of the object, which is related to the platform). The pointer in the object is called virtual function table pointer (v represents virtual, f represents function). A class containing virtual functions has at least one pointer to the virtual function table, because the address of the virtual function should be placed in the virtual function table, which is also referred to as the virtual table,. So what does this table put in the derived class? Let's go on to analyze:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};
class Driver :public Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 2;
};
int main()
{
	Base b;
	Driver d;
	cout << sizeof(b) << endl;
	cout << sizeof(d) << endl;
	return 0;
}
  • For the above code, we make the following modifications:
  • We add a derived class Derive to inherit Base.
  • Rewrite Func1 in Derive.
  • Base adds an imaginary function Func2 and an ordinary function Func3.
  • Through observation and testing, we found the following problems:
  • The derived class object d also has a virtual table pointer. The D object consists of two parts. One part is the member inherited from the parent class, and the virtual table pointer is the existing part, and the other part is its own member.
  • The virtual table of the base class b object is different from that of the derived class d object. Here, we find that Func1 has been rewritten, so the rewritten derived:: Func1 is stored in the virtual table of d, so the rewriting of virtual functions is also called overwriting. Overwriting refers to the overwriting of virtual functions in the virtual table. Rewriting is the name of grammar, and covering is the name of principle layer.
  • In addition, Func2 inherits from the virtual function, so it is placed in the virtual table, and Func3 inherits from it, but it is not a virtual function, so it will not be placed in the virtual table.
  • Virtual function table is essentially a pointer array storing virtual function pointers. Generally, a nullptr is placed at the end of this array.
  • Summarize the virtual table generation of derived classes: A. first copy the contents of the virtual table in the base class to the virtual table of derived classes. b. if the derived class rewrites a virtual function in the base class, use the derived class's own virtual function to cover the virtual function of the base class in the virtual table. c. The newly added virtual function of the derived class is added to the end of the derived class's virtual table according to its declaration order in the derived class.
  • Here is another question that children's shoes are easy to confuse: where do virtual functions exist? Where does the virtual table exist? Answer: virtual functions exist in virtual tables, and virtual tables exist in objects. Note that the answer above is wrong. But many children's shoes are so deeply convinced. Note that the virtual table stores virtual function pointers, not virtual functions. Virtual functions, like ordinary functions, have code segments, but their pointers are stored in the virtual table. In addition, the object is not a virtual table, but a virtual table pointer. So where does the virtual table exist? In fact, when we verify it, we will find that there are code segments under vs.
int main()
{
	Base b;
	Driver d;
	cout << sizeof(b) << endl;
	cout << sizeof(d) << endl;

	printf("vftptr Virtual table address:%p\n", *(int*)&b);
	//printf("vftptr virtual table address:% p\n",(int)b); Direct conversion syntax is not acceptable. Pointer conversion is used here to dereference

	int i = 0;
	int* p1 = &i;
	int* p2 = new int;
	char* p3 = "bit";
	printf("Stack variable:%p\n", p1);
	printf("Heap variable:%p\n", p2);
	printf("Snippet constant%p\n", p3);
	printf("Virtual function address:%p\n", &Base::Func1);
	printf("Ordinary function address:%p\n", &Base::Func3);

	return 0;
}

2. Principle of polymorphism

It has been analyzed for a long time. What is the principle of polymorphism? Remember that the Func function passes the Person::BuyTicket called by Person and the Student::BuyTicket called by Student.

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
	}
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-50% Off" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);
	return 0;
}

  • Looking at the green arrow in the figure below, we can see that when p points to mike object, P - > buyticket finds the virtual function in mike's virtual table, which is Person::BuyTicket.
  • Looking at the yellow arrow in the following figure, we can see that when p points to johnson object, P - > buyticket finds the virtual function in johson's virtual table, and the virtual function is Student::BuyTicket.
  • In this way, different objects show different forms when they complete the same behavior.
  • On the contrary, there are two conditions for us to achieve polymorphism, one is virtual function coverage, and the other is virtual function call by object pointer or reference. Reflect on why?
  • Through the following assembly code analysis, it can be seen that the function calls that meet polymorphism are not determined at compile time, but found in the object after running. If the polymorphic function call is not satisfied, it shall be confirmed at compile time.
#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-Full price" << endl;
	}
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Buy a ticket-50% Off" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);

	Mike.BuyTicket();
	return 0;
}
  • Conditions satisfying polymorphism:

  • The following conditions are not satisfied:
  • First of all, although BuyTicket is a virtual function, Mike is an object and does not meet the condition of polymorphism. Therefore, when the call of an ordinary function is converted into an address, the address of the function has been confirmed from the symbol table during compilation, and the call address is directly used

3. Dynamic binding and static binding

  • Static binding, also known as early binding, determines the behavior of the program during program compilation, also known as static polymorphism, such as function overloading.
  • Dynamic binding, also known as late binding (late binding), is to determine the specific behavior of the program and call specific functions according to the specific types during the running of the program, also known as dynamic polymorphism.
  • The assembly code for buying tickets explains well what static (compiler) binding and dynamic (runtime) binding are.

5, Virtual function table of single inheritance and multi inheritance relationship

It should be noted that in single inheritance and multi inheritance relationships, we will focus on the virtual table model of derived class objects, because we have seen the virtual table model of the base class before, and there is nothing to study in particular.

1. Virtual function table in single inheritance

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
private:
	int _a;

};
class Drive :public Base
{
public:
	virtual void func1()
	{
		cout << "Drive::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "Drive::func3()" << endl;
	}
	virtual void func4()
	{
		cout << "Drive::func4()" << endl;
	}
private:
	int _b;
};
int main()
{
	Base b;
	Drive d;
	return 0;
}

Looking at the monitoring window in the figure below, we find that func3 and func4 are not visible. Here is the compiler's monitoring window, which deliberately hides these two functions. It can also be regarded as a small bug. So how do we view the virtual table of d? Next, we use the code to print out the functions in the virtual table.

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
private:
	int _a;

};
class Drive :public Base
{
public:
	virtual void func1()
	{
		cout << "Drive::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "Drive::func3()" << endl;
	}
	virtual void func4()
	{
		cout << "Drive::func4()" << endl;
	}
private:
	int _b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// Print and call the virtual function pointer in the virtual table in turn. Call to see which function is stored
	cout << "Virtual table address>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("The first%d Virtual function address :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Drive d;
	// Idea: take out the first 4 bytes of b and d objects, which is the pointer of the virtual table. As we said earlier, the virtual function table is essentially a storage of virtual function pointers
	// 1. Take the address of b first and forcibly convert it to an int * pointer
	// 2. Dereference the value to get the 4bytes value of the b object header, which is the pointer to the virtual table
	// 3. Strong conversion to VFPTR *, because the virtual table is an array of VFPTR type (virtual function pointer type).
	// 4. Pass the virtual table pointer to PrintVTable to print the virtual table
	// 5. It should be noted that the code for printing the virtual table often crashes, because the compiler sometimes does not deal with the virtual table cleanly, and there is no at the end of the virtual table
	//Put nullptr, resulting in out of bounds. This is a compiler problem. We just need to click the - generate - clean solution in the directory bar and compile it.
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}


2. Virtual function table in multi inheritance

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base1 
{
public:
	virtual void func1() 
	{ 
		cout << "Base1::func1" << endl; 
	}
	virtual void func2() 
	{
		cout << "Base1::func2" << endl; 
	}
private:
	int b1;
};
class Base2 
{
public:
	virtual void func1() 
	{
		cout << "Base2::func1" << endl; 
	}
	virtual void func2() 
	{ 
		cout << "Base2::func2" << endl; 
	}
private:
	int b2;
};
class Derive : public Base1, public Base2 
{
public:
	virtual void func1() 
	{
		cout << "Derive::func1" << endl; 
	}
	virtual void func3() 
	{ 
		cout << "Derive::func3" << endl; 
	}
private:
	int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << "Virtual table address>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("The first%d Virtual function address :0X%x,->", 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;
}

It can be seen from the following figure that the UN overridden virtual functions of multiple inherited derived classes are placed in the virtual function table of the first inherited base class.

3. Diamond inheritance and diamond virtual inheritance

In practice, we do not recommend designing diamond inheritance and diamond virtual inheritance. On the one hand, it is too complex and easy to cause problems. On the other hand, such a model has a certain performance loss when accessing base class members. Therefore, we don't look at the virtual table of diamond inheritance and diamond virtual inheritance. Generally, we don't need to study it clearly, because it is rarely used in practice. If you are curious, you can see the following two link articles.
Analysis of C + + virtual function table
C + + object memory layout

6, Inheritance and polymorphism are common interview questions

1. Concept review

2. Question and answer

summary

The above is what we want to talk about today. This paper introduces the use of polymorphism in the three characteristics of C + +, and polymorphism provides a large number of functions and methods that enable us to process data quickly and conveniently. We must master it. In addition, if there are any problems above, please understand my brother's advice, but it doesn't matter. It's mainly because I can insist. I hope some students who study together can help me correct them. However, if you can be gentle, please tell me that love and peace are the eternal theme and love you.

Topics: C++