[C++] Polymorphism in C++

1. The concept of polymorphism

In layman's terms: polymorphism == multiple forms

Specifically: when different objects complete the same behavior, they will produce different states

Put it in a class: inherit different subclasses of a class, execute a function, and produce different results.

for example:

For example, when we buy tickets to go home during holidays , ordinary people buy tickets at full price, students buy tickets at half price, and soldiers buy tickets at priority.

2. Definition and implementation of polymorphism

2.1 Virtual functions

Modified virtualclass member functions are called virtual functions

class Person {
    
    
public:
	virtual void Buy_Ticket()
	{
    
    
		cout << "Person:全价" << endl;
	}
};
  • Among them, Buy_Ticketis a virtual function

2.2 Rewriting of virtual functions

There is a virtual function in the subclass that is exactly the same as the parent class (return value type, function name, and parameter list (the default value is not considered) are exactly the same), and the virtual function called the subclass rewrites the parent class. virtual function. (So ​​rewriting means rewriting the implementation of the corresponding virtual function of the parent class and inheriting its interface)

//子类添加virtual
class Person {
    
    
public:
	virtual void Buy_Ticket()
	{
    
    
		cout << "Person:全价" << endl;
	}
};

class Student :public Person {
    
    
public:
	virtual void Buy_Ticket()
	{
    
    
		cout << "Student:半价" << endl;
	}
};

//子类不添加virtual
class Person {
    
    
public:
	virtual void Buy_Ticket()
	{
    
    
		cout << "Person:全价" << endl;
	}
};

class Student :public Person {
    
    
public:
	void Buy_Ticket()
	{
    
    
		cout << "Student:半价" << endl;
	}
};
  • When rewriting a virtual function, subclasses do not use virtualit to constitute rewriting of the virtual function.

    This rule may be for the convenience of different people who can better implement the code when writing programs after inheriting the parent class, regardless of whether the members in the parent class are virtual functions. After all, as long as they are virtual functions, they will be rewritten , no need to use virtual in subclasses.

  • However, virtualit is not very standard for a subclass to override the parent class without using it, and it is not recommended to use it in this way.

(This paragraph briefly introduces the rewriting of virtual functions, and the application of rewriting virtual functions is as follows)

Two exceptions to virtual function overriding

  1. covariant

    The return value type of the virtual function of the parent class and the child class is different

    When the subclass overrides the virtual function of the parent class, the return value type of the virtual function of the parent class is different. That is, the virtual function of the parent class returns the pointer or reference of the parent class object, and the subclass returns the pointer or reference of the subclass object, which is called covariance .

    //返回本父类和子类的指针或引用
    class Person {
          
          
    public:
    	virtual Person* Buy_Ticket()
    	{
          
          
    		cout << "Person:全价" << endl;
    		return new Person();
    	}
    };
    
    class Student :public Person {
          
          
    public:
    	virtual Student* Buy_Ticket()
    	{
          
          
    		cout << "Student:半价" << endl;
    		return new Student();
    	}
    };
    
    //返回其它父类和子类的指针或引用
    class A{
          
          };
    class B : public A{
          
          };
    
    class Person {
          
          
    public:
    	virtual A* Buy_Ticket()
    	{
          
          
    		cout << "Person:全价" << endl;
    		return nullptr;
    	}
    };
    
    class Student :public Person {
          
          
    public:
    	virtual B* Buy_Ticket()
    	{
          
          
    		cout << "Student:半价" << endl;
    		return nullptr;
    	}
    };
    
    • As long as the reference or pointer of the parent class and subclass is returned, it doesn't matter whether it belongs to the class or not.
    • The pointer or reference of the subclass cannot be used as the return value of the virtual function of the parent class, and the pointer or reference of the parent class is used as the return value of the virtual function of the subclass. The compiler will report an error warning that the return value type does not match.
    • virtualIt is also possible to subclass without using the keyword
  2. Destructor override

    Parent and child destructors have different names

    If the destructor of the parent class is a virtual function, as long as the destructor of the subclass is defined, no matter whether the virtual keyword is added or not, it will be rewritten with the destructor of the parent class , although the names of the destructors of the base class and the derived class Different, it seems to violate the principle of rewriting, but in fact it is not. Here it can be understood that the compiler has made special processing on the name of the destructor, and the name of the destructor is uniformly processed after compilation destructor.

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

2.3 Constituent conditions of polymorphism

Polymorphism is the inheritance of different subclass objects of the same class, calling the same function to produce different behaviors.

For example: Student inherits from Person, the Person object buys the full price of the ticket, and the Student object buys the half price of the ticket.

Two conditions that constitute polymorphism in inheritance:

  1. 必须通过父类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

Let's take a look at the following categories, in the case of polymorphism and not polymorphism, the operation of the following code:

void test(Person& p){
    
     p.Buy_Ticket(); }

int main()
{
    
    
	Person p;
	Student s;
	
	test(p);
	test(s);

	return 0;
}
  • The test function parameter here can only be a reference or pointer of the parent class, otherwise it cannot be received normally when transferring the parent class object.

Satisfies polymorphism:

class Person {
    
    
public:
	virtual void Buy_Ticket()
	{
    
    
		cout << "Person:全价" << endl;
	}
};

class Student :public Person {
    
    
public:
	virtual void Buy_Ticket()
	{
    
    
		cout << "Student:半价" << endl;
	}
};

insert image description here

Here we can see that when polymorphism is satisfied, the member function of this type is called according to the type of the object pointed to by the pointer or referenced

Does not satisfy polymorphism:

  • Ordinary functions do not constitute virtual functions

    class Person {
          
          
    public:
    	void Buy_Ticket()
    	{
          
          
    		cout << "Person:全价" << endl;
    	}
    };
    
    class Student :public Person {
          
          
    public:
    	void Buy_Ticket()
    	{
          
          
    		cout << "Student:半价" << endl;
    	}
    };
    

    insert image description here

    Here we see that the member function in the parent class is not a virtual function, and the relationship between the parent and child classes is hidden, not rewritten, which does not meet the conditions of polymorphism. In this case, it depends on the type of pointer or reference . call a member function of this type

  • A destructor does not constitute a virtual function

    Before we talked about the rewriting of the destructor to make it a virtual function, many people may have questions about why the destructor should also be turned into a virtual function. Here we write a piece of code, take a look, destructor When the function is not a virtual function, the result of the operation does not meet our needs:

    //析构函数不使用virtual
    class A{
          
          
    public:
    	~A()
    	{
          
          
    		cout << "A()" << endl;
    	}
    };
    class B : public A{
          
          
    public:
    	~B()
    	{
          
          
    		cout << "B()" << endl;
    	}
    };
    
    int main()
    {
          
          
    	A* a = new A;
    	A* b = new B;
    	delete a;
    	delete b;
    
    	return 0;
    }
    

    insert image description here

    The destructor of class A is only called twice and the compiler reports an error, but the destructor of class B is not called, which is obviously wrong.

    • When delete breleasing the space of the b pointer, in the case that the destructor does not constitute polymorphism, the corresponding destructor is also called according to the type of the current pointer or reference, that is, the destructor of the parent class. This can lead to problems such as memory exposure
    //析构函数使用virtual
    class A{
          
          
    public:
    	virtual ~A()
    	{
          
          
    		cout << "A()" << endl;
    	}
    };
    class B : public A{
          
          
    public:
    	virtual ~B()
    	{
          
          
    		cout << "B()" << endl;
    	}
    };
    
    int main()
    {
          
          
    	A* a = new A;
    	A* b = new B;
    	delete a;
    	delete b;
    
    	return 0;
    }
    

    insert image description here

What does polymorphism do? Why use polymorphism when you can hide things that can be solved?

  • Like we want to do a function of buying tickets, different people have different fares, so we need to set up a parent class Person, multiple subclasses inherit the parent class, and create objects when using them. When we want to pass objects as parameters to a function to achieve For a specific function, we can’t create a parameter for each type of object (when there are too many classes, the code is redundant), we can only use the pointer or reference of the parent class to receive, and if there is no polymorphism in this function In the body, we only use members of the parent class, not other subclasses

    This is exactly the role of polymorphism, so that different objects call the same function, the results are different ,

    This is also the result that the compositional conditions of polymorphism force us to achieve.

  • The virtual function of C++ is born for rewriting, and rewriting is born for polymorphism. The grammatical concept of C++ here may be a little difficult to understand, because its underlying layer is very complicated.

2.4 C++11 override and final

It can be seen from the above that C++ has strict requirements on function rewriting, but in some cases, due to negligence, the alphabetical order of the function name may not constitute overloading, and this error will not be reported during compilation. Yes, but when the program does not get the expected results when it is running, it will not be worth the loss to debug to find the error. Therefore, C++11 provides two keywords override and final, which can help users detect whether to rewrite.

  1. final: Modifies the virtual function, indicating that the virtual function cannot be overridden

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

    error C3248: 'A::test' : function declared 'final' cannot be overridden by 'B::test'

  2. override: Check whether the virtual function of the subclass overrides a virtual function of the base class, and if not, compile and report an error.

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

    error C3668: 'B::test1' : method containing override specifier 'override' does not override any base class methods

2.5 Comparison of overloading, rewriting and hiding

insert image description here

3. Abstract class

3.1 Concept

Write it after the virtual function = 0, then the virtual function is a pure virtual function. A class containing pure virtual functions is called an abstract class (also called an interface class), and an abstract class cannot instantiate objects .

Features:

After the subclass inherits the abstract class, the object cannot be instantiated. Only after the subclass overrides the pure virtual function, the subclass can instantiate the object.

effect:

Pure virtual functions specify that subclasses must be rewritten, reflecting the inheritance of interfaces.

class A {
    
    
public:
	virtual void test() = 0;
};

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

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

int main()
{
    
    
	A a;//报错,A::test是纯虚函数
	B b;
	A c;//报错,A::test是纯虚函数

	return 0;
}

insert image description here

  • A type has no corresponding entity in reality, so we can define a class as an abstract class.
  • The difference between a pure virtual function and overridis that a pure virtual function: is applied in the parent class (mandatory rewriting), override: is applied in the subclass (checking the rewriting).

3.2 Interface inheritance and implementation inheritance

Implementation inheritance: Inheritance of ordinary functions, the subclass inherits the parent class, can use the function, and inherits the implementation of the function.

Interface inheritance: virtual function inheritance, the subclass inherits the interface of the virtual function of the parent class , the purpose is to rewrite, achieve polymorphism, and inherit the interface. So if you don't implement polymorphism, don't define functions as virtual functions

Four. The principle of polymorphism

4.1 Virtual function table

Let's take a look at a common written test question first, and see what the result of the following code is?

class A
{
    
    
public:
	virtual void test()
	{
    
    
		cout << "test()" << endl;
	}
private:
	int _a = 1;
    char _c;
};

int main()
{
    
    
	cout << "sizeof(A):" << sizeof(A) << endl;
	return 0;
}

We see that the result of the above code running is 12, which seems to be different from the size of the class we know. Normally, the size of a class is only related to its member variables, that is, _a occupies four bytes , _c occupies one byte, and the total size is an integer multiple of the largest member, which is 8, but the result is 12.

Because in this class, in addition to the two member variables, there is another one _vfptrplaced at the beginning of the space occupied by the object (note that some common ones may be placed at the end of the object, this is related to the platform, I use VS2019), the We call this pointer 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, and the virtual function is also referred to as the virtual table .

So what is placed in this table in the derived class? We write the following program to view:

class A
{
    
    
public:
	virtual void test()
	{
    
    
		cout << "test()" << endl;
	}
private:
	int _a = 1;
	char c;
};

int main()
{
    
    
	A a;
	
	return 0;
}

View the status of object a in the monitoring window through debugging, as shown in the figure below:

insert image description here

We see that the _vfptr pointer is the virtual function table pointer of the a object. Now we continue to modify the above code as follows, so that we can understand more things:

class A
{
    
    
public:
	virtual void test()
	{
    
    
		cout << "A::test()" << endl;
	}
	virtual void test1()
	{
    
    
		cout << "A::test1()" << endl;
	}
    void test2(){
    
    }
private:
	int _a = 1;
	char c;
};

class B : public A
{
    
    
public:
	virtual void test()
	{
    
    
		cout << "B::test()" << endl;
	}
private:
	int _b = 0;
};

int main()
{
    
    
	A a;
	B b;

	return 0;
}

insert image description here

  1. There is also a virtual table pointer in the subclass object b. The b object is composed of two parts, one part is a member inherited from the parent class, and the virtual table pointer also exists in this part, and the other part is its own member.

  2. The virtual tables of the parent class a object and the subclass b object are different. Here we find that test has been rewritten, so the rewritten B::test(void) is stored in the virtual table of b, so the rewritten virtual function Writing is also called coverage , and coverage refers to the coverage of virtual functions in the virtual table. Rewriting is called syntax, and coverage is called principle layer.

  3. In addition, test1 is inherited as a virtual function, so it is put into the virtual table (because there is no rewriting, the parent and child class objects use the same virtual function, you can observe the virtual function A::test1(void) of the parent and child class objects The address is the same), test2 is also inherited, but it is not a virtual function, so it is not placed in the virtual table.

  4. The essence of the virtual function table is an array of pointers that store virtual function pointers. Generally, this array has a nullptr value at the end, indicating that the array ends here.

    We use the memory window to view the virtual table of subclass objects, as shown in the following figure:

    insert image description here

    We observe the two pictures of the memory window and the monitoring window, and we can see that the first two addresses of the virtual table store the virtual function addresses of test and test1, and the last one stores the empty address, that is, nullptr.

  5. The derived class vtable is generated as follows:

    • First copy the content of the virtual table in the parent class to the virtual table of the subclass;
    • If the subclass overrides a virtual function in the parent class, overwrite the virtual function of the parent class in the virtual table with the subclass's own virtual function;
    • The virtual functions newly added by the subclass are added to the end of the virtual table of the subclass according to their declaration order in the subclass.

Notice:

Here's another confusing issue:

Where do virtual functions exist? Where does the virtual table exist?

Most of the answers are that virtual functions exist in virtual tables, and virtual tables exist in objects. Remember, the answer here is wrong, but many people think so deeply.

First of all, we need to understand whether the virtual table is stored in the object, but the pointer of the virtual table. The virtual function pointer is stored in the virtual table, not the virtual function. The virtual function is stored in the code segment just like the ordinary function. As shown below:

insert image description here

int main()
{
    
    
	A a;
	int b = 0;
	int* c = new int;
	static int d = 0;
	const char* e = "hello";

	printf("栈:%p\n", &b);
	printf("堆:%p\n", c);
	printf("数据段:%p\n", &d);
	printf("代码段:%p\n", e);
	printf("虚表:%p\n", *(int*)&a);

	return 0;
}

insert image description here

in conclusion:

  • Both virtual tables and virtual functions exist in the code segment

4.2 The principle of polymorphism

Through the above study, we know the existence of virtual tables in polymorphism. Now let's take a look at how polymorphism is realized by using virtual tables.

(1) Code Analysis

class Person {
    
    
public:
	virtual void Buy_Ticket() {
    
     cout << "买票-全价" << endl; }
};
class Student : public Person {
    
    
public:
	virtual void Buy_Ticket() {
    
     cout << "买票-半价" << endl; }
};
void Func(Person* p)
{
    
    
	p->Buy_Ticket();
}
int main()
{
    
    
	Person* pptr = new Person;
	Student* sptr = new Student;

	Func(pptr);
	Func(sptr);

	return 0;
}

insert image description here

  1. Observe the red arrow in the above figure: when p points to the pptr object, p->Buy_Ticket finds the virtual function Person::Buy_Ticket in the virtual table of pptr.
  2. Observe the blue arrow in the figure above: when p points to the sptr object, p->Buy_Ticket finds the virtual function Student::Buy_Ticket in the virtual table of sptr.
  3. In this way, when different objects complete the unified behavior, they show different forms.

question:

There are two conditions to achieve polymorphism, one is the virtual function coverage, and the other is the pointer or reference of the object to call the virtual function, why? Is it ok to receive with normal object?

  1. Virtual function table override:

    First of all, a class will have a virtual function table only if there are virtual functions in it, and the address of the virtual function can be stored in it, and then there will be a function pointed to by the call.

    Secondly, virtual function table coverage is to make different classes point to the same function to produce different results, that is, to execute different results. Only when the virtual function table is covered can we have different things for us to execute, otherwise polymorphism Meaningless.

  2. Object pointer or reference:

    This must be viewed in conjunction with the inherited slice. In the parent class object and reference, the address of the member of the parent class object in the subclass object is received, so the member of the subclass object is called at this time.

    And when there are too many subclass objects, you can use the parent class object to receive them. After receiving them, they still point to the members of the corresponding subclass objects, and you can still find the virtual function to be called in the corresponding virtual table.

    insert image description here

  3. Receive using object:

    As shown in the figure above, when the parent class object receives the subclass object, it is copied. The question is whether the virtual table of the subclass object is copied at this time. If the virtual table is copied, the corresponding virtual function can still be called to execute If there is no desired result, then it is an ordinary parent class object, and the virtual function of the parent class is executed.

    In order to make the result more obvious, we assume that when the members of the parent and child classes have only one virtual function and nothing else, 父类 = 子类copying the virtual table of the subclass occurs during the process, then is the parent class object a child class object or a parent class object? class object?

    It is said that it is a parent class object, and its virtual function is different from other parent class objects, but it is the same as that of the subclass object. It is said that it is a subclass object, and its type is the parent class object. , is my own son again", what kind of weirdness is this?

    Having said that, the answer is ready to come out, the use of object copy is not allowed.

(2) Clean up the solution

When we compile the program and view the corresponding memory and data changes in the debug monitor and memory window, what we may get is different from what we expected. This may not be because of a problem with your code, but because there is no problem after allowing the program before. To clean up and clean up the errors caused, generally at this time, click on the cleaning solution at the position in the picture below to solve the problem.

insert image description here

Cleaning up solutions is a very important function. Its main purpose is to clear the compiled output files and intermediate files of all projects in the solution to ensure that on the next rebuild, all files are recompiled from scratch instead of using the previous cached files.

Cleaning up solutions can help developers address the following issues:

  1. Free up disk space : When there are many projects in a solution, each project generates a large number of intermediate files and compilation output files. These files can take up a lot of disk space, which can be freed by cleaning solutions.
  2. Resolving build errors : Occasionally when building a solution you get some weird compilation errors, which can be caused by corrupted or outdated cache files. Cleaning up the solution can fix these problems.
  3. Make sure to rebuild : Sometimes after modifying the code, some files are not recompiled, causing the program to run incorrectly. Cleaning the solution ensures that all files are recompiled.

4.3 Dynamic binding and static binding

  1. Static binding is also called early binding (early binding), which degrades 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 according to the specific type obtained during the running of the program, and call specific functions, also known as dynamic polymorphism.

Both are actually determined at compile time, but static polymorphism is to write callthe address to death at compile time, while dynamic polymorphism is to find the address in the virtual table at runtime, but the virtual table is determined at compile time OK

Five. Virtual function table of single inheritance and multiple inheritance

Next, let's take a look at the virtual table model of subclass objects in single inheritance and multiple inheritance relationships, because we already know the virtual table model of parent class objects, and there is nothing more to study.

5.1 Virtual function table in single inheritance

(1) Virtual functions in subclasses

class A {
    
    
public:
	virtual void test1() {
    
     cout << "A::test1" << endl; }
	virtual void test2() {
    
     cout << "A::test2" << endl; }
private:
	int a;
};
class B :public A {
    
    
public:
	virtual void test1() {
    
     cout << "B::test1" << endl; }
	virtual void test3() {
    
     cout << "B::test3" << endl; }
	virtual void test4() {
    
     cout << "B::test4" << endl; }
private:
	int b;
};

Looking at the figure below, we found that the virtual functions test3 and test4 in subclass B are not displayed in the monitoring window:

insert image description here

Here, these two functions are intentionally hidden in the monitor window of the compiler, which can also be regarded as a small bug. Next, let’s check in the memory window first. There are indeed four function addresses in the vtable of object b.

insert image description here

We see that there are indeed 4 virtual function addresses in the b object, that is, test3 and test4 also exist in the virtual table.

Print vtable:

Next we use the code to print out the functions in the vtable. (Show the code first, then analyze the code)

void PrintVTable(VFPTR* VTable)
{
    
    
	for (int i = 0; VTable[i] != nullptr; i++)
	{
    
    
		printf("[%d]:%p->", i + 1, VTable[i]);
		VFPTR f = VTable[i];
		f();
	}
	cout << endl;
}

int main()
{
    
    
	A a;
	B b;
    //VFPTR* VTable = (VFPTR*)(*(int*)&a);
	VFPTR* VTableA = *(VFPTR**)&a;
    
    //VFPTR* VTable = (VFPTR*)(*(int*)&b);
    VFPTR* VTableB = *(VFPTR**)&b;
	PrintVTable(VTableA);
	PrintVTable(VTableB);

	return 0;
}

insert image description here

We see that the printing of the virtual function table is the same as what we saw in the memory window, and there are four virtual functions in the virtual function table of the b object.

  • Note: Observing the above figure, we can find that the virtual functions in the virtual table are stored in the order of declaration, and when the virtual functions of different objects are called through polymorphism, it is the declaration order of the virtual functions that is declared first. The front of the function table, the last declaration is at the back, and the order is more orderly to find the position of the corresponding virtual function.

Next, let's analyze the code for viewing the virtual table above:

  1. First, the address of the virtual function is stored in the virtual table, so we need to define a function pointer type to store these addresses:

    typedef void (* VFPTR)();
    
  2. Secondly, with the type of storing the address in the virtual table, the next step is to turn the virtual table into an array, store it and print it, and we know that the virtual table ends with nullptr, so we only need to know the initial value of the virtual table Addresses are traversed sequentially until the value in the address is nullptr . You can get this virtual table:

    Through the above memory diagram and the more intuitive storage logic diagram below, we know that the first address of the object stores the address of the virtual table, that is, the first address of the virtual table, so the first four bytes of the object should be taken value , there are the following two methods.

    insert image description here

    • method 1:

      	B b;
      	VFPTR* VTable = (VFPTR*)(*(int*)&b);//32字节下有效
      

      First, &b represents the first address of the b object, and we convert it to (int*) type (only after it is converted to int* type, &b at this time represents the four-byte address pointing to the first address, which can only be dereferenced after Get an int type value, that is, the value of 4 bytes, the address of the corresponding virtual table), under 32 bits, we get the first address of the b object.

      Secondly, under 32 bits, the size of the pointer is 4, and the size of the int type is also 4. By dereferencing, a value of the int type is obtained, and the size of the value is 4 bytes, that is, 4 words in the first address are obtained. The vtable address of the section.

      Finally, after converting it into VFPTR*the pointer type of the function pointer, the value in the virtual table address is the address of the virtual function, and the address type of the virtual function is VFPTR, so the type of the pointer storing the address of the virtual function is VFPTR*determined by a corresponding type Received by the variable VTable, which represents the virtual function table array.

    • Method 2:

      	B b;
      	VFPTR* VTable = (*(VFPTR**)&b);
      

      This method is easy when you think about it, but difficult if you don’t think about it, unlike the previous step-by-step method.

      insert image description here

      First, we convert the virtual table pointer storing the address of the virtual table, that is, the first address of the b object, into a second-level pointer of the function pointer type. At this time, the first address is a second-level pointer (strictly speaking, a third-level pointer)

      Secondly, if we want to get the value in the second-level pointer, we only need to dereference and get the address of the virtual table through one dereference. The type of the address at this time is , just receive it directly VFPTR*.

    • Method 1 can only be used under 32-bit. If it is used under 64-bit, you need to change int to double or long long. Method 2 can be used on both 32-bit and 64-bit.

  3. Note: When obtaining the address of the virtual table, do not write the code as follows:

    	B b;
    	VFPTR* VTable = (VFPTR*)&b;
    

    Because &b is the address of the first element of the object, and this operation only converts the address of the first element of the object, that is, the type of the virtual table pointer, to VFPTR*obtain the virtual table pointer, not the address of the value virtual table in the virtual table pointer.

    And after our analysis above, the type of the virtual table pointer is VFPTR**, so it is also wrong to write this way.

Two problems with virtual tables:

When was the virtual table created?

Through the above knowledge, we know that the memory of the virtual table is the address of the virtual function corresponding to the class, so it is in the compilation stage, because only in the compilation stage will the address of the function be generated.

When is the virtual table pointer initialized?

Initialized when the constructor's initializer list is executed.

As shown in the figure below, we execute the code and observe the change of the virtual table pointer when the object calls the constructor

insert image description here

As shown above, the virtual function table pointer is initialized in the initialization list of the constructor.

(2) Virtual functions of subclass multi-objects

int main()
{
    
    
	B b1;
	B b2;

	printf("b1:%p\n", *(VFPTR**)&b1);
	printf("b2:%p\n", *(VFPTR**)&b2);
	cout << endl;
	PrintVTable(*(VFPTR**)&b1);
	PrintVTable(*(VFPTR**)&b2);

	return 0;
}

insert image description here

  • As shown in the figure above, when a subclass object creates multiple objects, the same vtable is used.

5.2 Virtual function table in multiple inheritance

class B {
    
    
public:
	virtual void test1()
	{
    
    
		cout << "B::test1()" << endl;
	}
	virtual void test2()
	{
    
    
		cout << "B::test2()" << endl;
	}
};

class C {
    
    
public:
	virtual void test1()
	{
    
    
		cout << "C::test1()" << endl;
	}
	virtual void test2()
	{
    
    
		cout << "C::test2()" << endl;
	}
};

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

int main()
{
    
    
	D d;

	return 0;
}

insert image description here

We can see from the above figure that in the d object, because it inherits two classes, there are two virtual tables , namely B and C. If you are careful enough, you should find that these two virtual tables have the following two question:

  1. The test3 function of the d object is in that virtual table?

    We write the following code to print the virtual table to view, test3 is in that virtual table:

    typedef void(*VFPTR)();
    
    void PrintVTable(VFPTR* VTable)
    {
          
          
    	for (int i = 0; VTable[i] != nullptr; i++)
    	{
          
          
    		printf("[%d]:%p->", i + 1, VTable[i]);
    		VFPTR f = VTable[i];
    		f();
    	}
    	cout << endl;
    }
    
    int main()
    {
          
          
    	D d;
    
    	PrintVTable((VFPTR*)*(int*)&d);
    	PrintVTable((VFPTR*)*((int*)&d + sizeof(B)/4));
    
    	return 0;
    }
    

    insert image description here

    Because the class inherited first is defined first, so the first printed virtual table is the virtual table of B, and the second printed is the virtual table of C, so we can conclude that the overloaded ones are not implemented in the subclasses . The virtual function will exist in the virtual table of the first inherited class, that is, the first virtual table.

    Notice:

    When printing the second virtual table, it should be noted that the first virtual table is at the head of the space where the object is located, and the second virtual table is not necessarily the next position next to the first virtual table. The space in the object The storage structure is as follows:

    insert image description here

    Whether the address of the first virtual table is adjacent to the address of the second virtual table depends on whether there are other members of the first class that will be inherited.

    So when we get the address of the second virtual table, we have the following two options:

    • plan 1:

      	//1
      	VFPTR* VTable = (VFPTR*)*((int*)&d + sizeof(B)/4);
      	//2
      	VFPTR* VTable = (VFPTR*)*(int*)((char*)&d + sizeof(B));
      

      First, use sizeof to obtain the size of class B, that is, the space occupied by class B in the d object space.

      Secondly, (under 32-bit) when the address of the object is forced to int*, every addition of 1 means moving the size of an int (4 bytes), so divide the total size of the obtained B-class space by 4, know A total of 4 bytes have to be moved several times.

      When the address of the object is forced to char*, adding 1 means moving the size of a char type (1 byte), and directly moving the total size of the B-type space. Note here, move to the second virtual table After the first address of the address, the space type of the address needs to be changed to int*, otherwise the 4-byte address cannot be obtained after dereferencing.

    • Scenario 2:

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

      Using slicing, to obtain the virtual table corresponding to class C, you only need to use the pointer variable of class C to receive the address of object d. At this time, the first address pointed to by pointer c will be the virtual table corresponding to class C of object d, and then Pass the value normally.

  2. Why class D rewrites the virtual function test1, but the addresses of the two test1s of its parent class are different?

    insert image description here

    Because the string after the address is printed according to the function called, and the two vtables print the same D::test1(), and the above code fully complies with the two rules that constitute polymorphism, their addresses should be the same That's right, why is the address different?

    To facilitate testing, write the following code:

    int main()
    {
    	D d;
    	B* b = &d;
    	C* c = &d;
        
    	b->test1();
    	
        c->test1();
    
    	return 0;
    }
    

    Compile the running diagram as follows:

    insert image description here

    As shown in the above figure, the pointer c takes two more steps than the pointer b, and finally points to the same address and calls the same function.

    Looking at the picture above, the first step is the same, but there is a difference in the middle two steps of the c pointer. Why can't the c pointer directly omit the middle two steps? We see that in the middle two parts of the c pointer, the first step is a jmpjump execution subinstruction, and then jmpit comes to the final position. Why can't it jmpjump directly to the appropriate position when executing the first instruction?

    The reason lies in subthe instruction, and the back of the line where it is located ecx,4means to move the current position backward by 4 bytes.

    Now let's take a look at the space content of the d object, as shown in the figure below:

    insert image description here

    As shown in the figure above, the reason why the b object can directly find the virtual function is because the this pointer of the d object points to the address of the first element of the d object, and the storage location of the first inherited class starts from the first address, so when calling When the virtual function needs to pass the this pointer, it is passed directly.

    The members and virtual table of class C exist under class B, its address is not the first address pointed to by this pointer, and the member functions in the class have hidden this pointer, and the pointer must be a pointer to the calling class object , so it needs to be offset before it can be used.

    By observing the assembly code, we know that the c pointer has moved 4 bytes backwards, while class B stores only one virtual table address, which is 4 bytes under 32 bits, and moves back four bits after the offset to satisfy the this pointer transmission.

    Note: Whether the pointer moves depends on whether it is the first inherited class or not. The moving distance is related to the distance of the specific first address, that is, the size of each class inherited before the class to be moved in the corresponding object.

(The space address here will be different in the context, it is the structure after I run it at different times, and it will not affect reading and learning)

5.3 Virtual table and virtual base table

In the inheritance of C++, we learned that in order to solve the diamond-shaped inheritance, a diamond-shaped virtual inheritance was generated, and the generation of the diamond-shaped virtual inheritance led to the generation of a virtual base table. The virtual base table is only 8 bytes in size, and the four bytes before and after each Represents an offset. When there is no polymorphism in the code, the first offset is 0, and the second offset indicates the offset of the public member from the current position. We write the following program and view it through the memory window Changes in the virtual base table in diamond virtual inheritance when polymorphism is present.

class A {
    
    
public:
	virtual void test(){
    
    }
};

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

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

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

int main()
{
    
    
	D d;

	return 0;
}

insert image description here

As shown in the figure above, in the space of object d, in the 8-byte space occupied by class B, the first 4 bytes represent the address of the virtual table, and the last four bytes represent the virtual base table. The same is true for class C, and The space occupied by the public parent class A is below, and only the address of a virtual table is stored.

Then we can draw the following conclusions:

In the address pointed to by the C++ virtual base table pointer, the first four bytes point to the offset of the virtual table pointer where the corresponding virtual table is located in the space (moving four bytes backward to find the virtual table pointer). The last four bytes are the offset of the virtual base class (class B moves forward 12 bytes to find the class A space), which is used to calculate the address of the virtual base class in the derived class object.

5.4 Polymorphism in Diamond Inheritance

As the following code:

class A {
    
    
public:
	virtual void test(){
    
    }
};

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

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

class D : public B, public C {
    
    
public:

};

Classes B and C rewrite the virtual functions of the parent class A at the same time, and when class D inherits classes B and C, the compiler will burst out an error, because it is impossible to determine which rewritten virtual function to use, so the syntax at this time It is stipulated that when multiple parent classes rewrite the same virtual function, the subclass must rewrite it, and finally use the virtual function rewritten by the subclass.

In practice, we do not recommend designing diamond-shaped inheritance and diamond-shaped virtual inheritance. On the one hand, it is too complicated and prone to problems. On the other hand, such a model has a certain performance loss when accessing base class members. So we don't look at the rhombus inheritance and rhombus virtual inheritance of our vtables, and generally we don't need to study them clearly, because they are rarely used in practice. If you are interested, you can read the two linked articles below.

  1. C++ virtual function table analysis
  2. Memory layout of C++ objects

Guess you like

Origin blog.csdn.net/m0_52094687/article/details/130669329