Explore polymorphism in C + +

Posted by hadingrh on Thu, 10 Feb 2022 12:37:05 +0100

Concept of polymorphism 1

  • 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.

2 Definition and implementation of polymorphism

2.1 composition conditions of polymorphism

Polymorphic conditions are introduced with the help of code

class Person{
public:
	virtual void  BuyTickets()
	{
		cout << "Person,Full price" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Student,50% Off" << endl;
	}
};

void Buy(Person& sp)
{
	sp.BuyTickets();
	
}
int main()
{
	Person p;
	Student s;
	Buy(p);
	Buy(s);
	return 0;
}

In this way, polymorphism is realized. There are two conditions for polymorphism:

  • 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 rewrite (overwrite) the virtual function of the base class, so the rewriting also needs to meet the following conditions: a. the functions in the parent and child classes are virtual functions modified by virtual; b. the function name, parameters and return value must be the same


be careful

When rewriting the virtual function of the base class, the virtual function of the derived class does not add the virtual keyword. Although it can also constitute rewriting (because the derived class inherits the properties of the virtual function in the base class), this writing method is not standardized. Do not write it like this.
Not satisfied with polymorphism: it is related to the type (on the left), that is, what type sp is, call the member function in this class; Satisfy polymorphism: it is related to the object, that is, which object is called when pointing to that object (right).
The size of the calculation object needs to be calculated by 4 bytes (4 bytes for 32 bits and 8 bytes for 64 bits), because there is a virtual base table pointer (explained later)

2.2 what is a virtual function

  • The member function of a class modified by virtual is a virtual function
class Person {
public:
 virtual void BuyTicket() { cout << "Buy a ticket-Full price" << endl;}
}

2.3 two exceptions to virtual function rewriting

2.3.1 covariance (understanding)

  • 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.
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.3.2 rewriting of destructor (a high-frequency interview question)

  • If the destructor of the base class is a virtual function, the derived class will be rewritten with the destructor of the base class as long as it is defined, regardless of whether the virtual keyword is added or not, although the name of the destructor of the base class is different from that of the derived class. It seems that the rewriting rule is violated, but it is not. It can be understood here that the compiler has made special processing on the destructor, and the destructor name is uniformly processed into destructor after compilation

Why is the destructor of the base class in C + + best defined as a virtual function?

  • If the destructor of the base class is not defined as a virtual function, the destructor of the subclass in the inheritance system cannot override the destructor of the parent class, and the condition of polymorphism cannot be met. In special cases, there will be memory leakage. The code is shown below
#include <iostream>
using namespace std;

class Person{
public:
	 ~Person(){
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student(){
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person p;
	Student s;

	return 0;
}

  • In the above scenario, without adding virtual = > to the base class destructor, it does not constitute rewriting = > it does not constitute polymorphism, so it can be destructed normally, as shown in the following figure
  • But if this is the case
#include <iostream>
using namespace std;

class Person{
public:
	 ~Person(){
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	 ~Student(){
		cout << "~Student()" << endl;
	}
};
int main()
{
	// Only when the destructor of the derived class Student overrides the destructor of Person, and the destructor is called by the delete object below, can it be used
	// Only by forming polymorphism can we ensure that the objects pointed to by p1 and p2 call the destructor correctly.
	Person* p1 = new Person;
	delete p1;
	Person* p2 = new Student;
	delete p2;
	//For p2, only the part of the parent class is destructed, while the destructor of the child class Student is not called
	//If a subclass applies for resources and you don't call the subclass destructor to release resources to the system, it will
	//The memory resource is leaked because we didn't add virtual to the destructor of the base class
	//Resulting in no polymorphism.

	//Summary: make up the object pointed to by the polymorphic call
	//      If there is no polymorphism, it is called according to the type
	//Therefore, when designing base classes, it is best to design destructors as virtual functions
	return 0;
}


The modification code is as follows:

#include <iostream>
using namespace std;

class Person{
public:
	virtual ~Person(){
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	virtual ~Student(){
		cout << "~Student()" << endl;
	}
};
int main()
{
	// Only when the destructor of the derived class Student overrides the destructor of Person, and the destructor is called by the delete object below, can it be used
	// Only by forming polymorphism can we ensure that the objects pointed to by p1 and p2 call the destructor correctly.
	Person* p1 = new Person;
	delete p1;
	Person* p2 = new Student;
	delete p2;
	//For p2, only the part of the parent class is destructed, while the destructor of the child class Student is not called
	//If a subclass applies for resources and you don't call the subclass destructor to release resources to the system, it will
	//The memory resource is leaked because we didn't add virtual to the destructor of the base class
	//Resulting in no polymorphism.

	//Summary: make up the object pointed to by the polymorphic call
	//      If there is no polymorphism, it is called according to the type
	//Therefore, when designing base classes, it is best to design destructors as virtual functions
	return 0;
}

2.4 two keywords related to rewriting in C + + 11

As can be seen from the above code, C + + has strict requirements for function rewriting, but in some cases, due to its own negligence, the function name is written incorrectly and cannot constitute rewriting, and this error will not be reported during compilation. Only when the program does not get the expected results when running, it debug s itself and finds the root of the problem for a long time or even several days, This is not worth the loss. Therefore, C++ 11 provides two keywords: override and final, which can help users detect whether to rewrite

  • final: modifies a virtual function, indicating that the virtual function cannot be inherited or rewritten; If you modify a class, it cannot be inherited, that is, it cannot have subclasses
class Car 
{
public:
	virtual void Drive() final  {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-comfortable" << endl; }
};

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

  • override: check whether the derived class function rewrites a virtual function of the base class. If it is not rewritten, write and compile an error.
class Car
{
public:
	 void Drive() {}
};
class Benz :public Car
{
public:
	virtual void Drive() override { cout << "Benz-comfortable" << endl; }
};

2.5 comparison of overloading, overwriting (Rewriting) and hiding (redefining) (focus on understanding and distinguishing)

3 abstract class

3.1 concept of abstract class

If = 0 is added after the virtual function, the function is a pure virtual function. Classes containing pure virtual functions are called abstract classes (also known as interface classes). Abstract classes cannot instantiate objects. After the derived class inherits the abstract class, the object can be instantiated only by overriding the pure virtual function. Otherwise, the derived class is also an abstract class. Pure virtual functions regulate that derived classes must override inherited virtual functions. In addition, pure virtual functions embody interface inheritance.

class Car
{
public:
	virtual void Drive() = 0;//Pure virtual function
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-comfortable" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-Manipulation" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive(); 

	Car* pBMW = new BMW;
	pBMW->Drive();
}
int main()
{
	Test();
	return 0;
}

3.2 interface inheritance and implementation inheritance

The inheritance of ordinary functions is a kind of implementation inheritance. Derived classes inherit the functions of the base class and can use functions. What they inherit is the implementation of functions. The inheritance of virtual function is a kind of interface inheritance. The derived class inherits the interface of the base class virtual function. The purpose is to rewrite and achieve polymorphism. What is inherited is the interface. So don't define a function as a virtual function without realizing polymorphism.

4 principle of polymorphism (old and important)

4.1 virtual function table

In order to fully explain the principle of polymorphism, the concept of virtual function table (array referred to by table) needs to be introduced.

Observe the following code. What should be the result?

class Base{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	return 0;
}

Do you think it's 4 bytes, but it's not.

It is 8 bytes. We check the b object through the monitoring window and find that except_ b member variable, one more_ vfptr pointer is placed in front of the object (Note: some platforms may be placed at the back of the object, which is related to the platform). This pointer in the object is called virtual function table pointer (v represents virtual, f represents function). There is at least one virtual function table pointer in a class containing virtual functions, because the address of virtual functions should be placed in virtual functions, Virtual function table is also called virtual table for short. Virtual function table is essentially an array of function pointers.

For the above code to do the next transformation
1. We add a derived class Derive to inherit Base
2. Rewrite Func1 in Derive
3. Base adds an imaginary function Func2 and an ordinary function Func3

#include <iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "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()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	cout << sizeof(Base) << "byte" << endl;
	return 0;
}

int main()
{
	Base b1;
	Base b2;
	
	Derive d;
	cout << sizeof(Base) << "byte" << endl;
	return 0;
}

Three questions?
1. Are the virtual tables of b1 and b2 the same?
2. Where does the virtual table exist?
3. When is the virtual table generated?

  • As shown in the figure below, it is clear that the virtual function tables of b1 and b2 are the same

  • Look at the following code and result diagram to answer the second question, then the virtual function table pointer has a code segment (constant area)
    Base b1;
	Base b2;
	//To verify where the virtual table exists, write the following code
	printf("Virtual table pointer:%p\n", *((int*)&b1));
	int a = 0;
	static int b = 0;
	int *p = new int;
	char* str = "hello world";
	printf("Stack:%p\n", &a);
	printf("Data segment(Static area):%p\n", &b);
	printf("heap:%p\n", p);
	printf("Code snippet(Constant area):%p\n", str);

  • The virtual function table is generated at compile time

Through the above code, we will explore how polymorphism is embodied and implemented in the single inheritance system

    Base b;
	Derive d;


We can draw the following conclusions:

  • The derived class object d also has a virtual table pointer. The D object is composed of two parts. One part is inherited from the parent class, including the virtual table pointer. It can be understood as copying the continuation table of the base class to the child class, but the child class rewrites the virtual function Func1 of the parent class, so Func1 is overwritten in the continuation table of the child class, so the situation in the above figure is obtained.
  • The virtual tables of the base class b object and the derived class d object are different. Here we find that Func1 has been rewritten, so the rewritten Derive::Func1 exists 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.
  • Func2 inherits from the virtual function, so it is also put into the virtual table, and Func3 inherits from it, but it is not a virtual function, so it will not be put into the virtual table.
  • Virtual function table my essence is a pointer array storing virtual function pointers. At the end of this array, there is a nullptr (figure above)
  • Summarize the generation of the virtual function table of the derived class: A. first copy the content of the virtual table in the base class to the virtual table of the derived class. b. if the derived class rewrites a virtual function in the base class, use the virtual function of the derived class to cover the virtual function of the base class in the virtual table. c. the virtual function newly added by the derived class itself is added to the end of the virtual table of the derived class according to the declaration order in the derived class.
  • There is also an easily confused question: where do virtual functions exist? Where does the virtual table exist? Many people may answer this: virtual functions exist in virtual tables, and continuation tables exist in objects. Note: This is a big mistake. It should be like this: the virtual function table stores the virtual function pointer (the address where the virtual function is located), not the virtual function. The virtual function has code segments like ordinary functions, and the virtual function table pointer is an address in the object. Where the continuation table exists, which we have verified above, is also a code segment.

4.2 principle of polymorphism

With such a large virtual function table, we finally have to talk about the 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
#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 laoyang;
	Student xiaochen;
	Func(laoyang);
	Func(xiaochen);
	return 0;
}

  • Looking at the red arrow in the figure below, we can see that when p points to laoyang object, P - > buyticket finds the virtual function in mike's virtual table, and the virtual function is Person::BuyTicket.
  • Looking at the green arrow in the figure below, we can see that when p points to xiaochen object, P - > buyticket finds the virtual function in johson's virtual table, which 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 is not satisfied, it shall be confirmed at compile time.
// The following assembly code that is not related to your problem has been removed
void Func(Person* p) {
...
p->BuyTicket();
// Stored in P is the pointer of mike object. Move p to eax
001940DE mov eax,dword ptr [p]
// [eax] is the content pointed to by the eax value. Here, it is equivalent to moving the first four bytes (virtual table pointer) of the mike object to edx
001940E1 mov edx,dword ptr [eax]
// [edx] is the content pointed to by the edx value. Here, it is equivalent to moving the virtual function pointer stored in the first 4 bytes of the virtual table to eax
00B823EE mov eax,dword ptr [edx]
// Pointer of virtual function stored in call eax. It can be seen here that the calls that meet polymorphism are not determined at compile time, but after running
 Take the image in the.
001940EA call eax 
00 Head 1940 EC cmp esi,esp 
}
int main()
{
... 
// First of all, although BuyTicket is a virtual function, mike is an object and does not meet the condition of polymorphism, so here is the conversion from the call of ordinary functions to
 Address refers to the address of the function that has been confirmed from the symbol table during compilation call address
 mike.BuyTicket();
 00195182 lea ecx,[mike]
 00195185 call Person::BuyTicket (01914F6h) 
... 
}

4.3 dynamic binding and static binding

  1. 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
  2. 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 operation of the program, which is also known as dynamic polymorphism.
  3. The assembly code for buying tickets well explains what static (compiler) binding and dynamic (runtime) binding are.

Polymorphism can be seen everywhere in life, so just as art comes from life, so does technology. When you understand knowledge and technology, you will understand it more deeply by analogy with life.

Topics: C++ Polymorphism