This time we will look at the virtual inheritance of the diamond structure. The introduction of virtual inheritance aims to solve the problem of the inheritance system of complex structures. In the previous article, we used a simple inheritance structure when discussing virtual inheritance, just to pave the way.
Let's take a look at these classes. This is a typical diamond inheritance structure. C100 and C101 share the same parent class C041 through virtual inheritance. C110 is inherited from C100 and C101.
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
struct C100 : public virtual C041
{
C100() : c_(0x02) {}
char c_;
};
struct C101 : public virtual C041
{
C101() : c_(0x03) {}
char c_;
};
struct C110 : public C100, public C101
{
C110() : c_(0x04) {}
char c_;
};
Run the following code:
PRINT_SIZE_DETAIL(C110)
Result:
The size of C110 is 16
The detail of C110 is 28 c3 45 00 02 1c c3 45 00 03 04 18 c3 45 00 01
We can plot the memory layout of an object as in the previous article.
|C100,5 |C101,5 |C110,1 |C041,5 |
|ospt,4,11 |m,1 |ospt,4,6 |m,1 |m,1 |vtpt,4 |m1 |
(Note: I used the abbreviation to keep the rows straight. Ospt indicates the offset value pointer, m indicates the member variable, and vtpt indicates the virtual table pointer. The first number is the size of the region, that is, the number of bytes. Only the offset value pointer has a second number, and the second number is the size of the Offset Value pointed to by the offset value pointer .)
We can see that there is only one C041 in the memory layout of the object, that is, there is only one part of the grandfather class, and it is placed at the end. This is Diamond inheritance. Compared with the previous discussions, we can know that if the virtual Inheritance Mechanism is not used, two C041 parts will appear in the memory layout of the C041 object, which is also called the V-type inheritance. The corresponding object layout is C041 + C100 + C041 + C101 + C110. In V-type inheritance, you cannot directly transform from C110, that is, the grandson class, to C041, that is, the grandfather class. Because there are two grandfathers in the object layout, one from C100 and the other from C101. The compiler has ambiguity in the resolution. It does not know which entity is used after the transformation. Although it can be solved by first transforming to a parent class and then to a grandfather class. However, when this method is used, if the content of the member variables of the grandfather class is rewritten,RuntimeIt does not synchronize the statuses of two grandpaid entities, so there may be semantic errors.
Let's analyze the memory layout above. General inheritance layout, top-level class in front. Multiple inheritance is arranged from left to right. The inheritance from C100 and C101 to C110 is normal inheritance, so following this principle, first the left parent class and then the right parent class, and then the Child class. Virtual inheritance requires that the shared parent class be placed at the end of the entire object layout (even if the virtual parent class is not truly shared, this is the case for the C020 class in the previous article. I don't know if the optimization switch will change .) Therefore, the grandfather class in the above example is placed at the end.
Let's look at the access to members. Run the following code and view the corresponding assembly code.
C110 c110;
c110.c_ = 0x51;
c110.C100::c_ = 0x52;
c110.C101::c_ = 0x52;
c110.C041::c_ = 0x53;
c110.foo();
The corresponding assembly code is:
01 00423993 push 1
02 00423995 lea ecx,[ebp+FFFFF7F0h]
03 0042399B call 0041DE60
04 004239A0 mov byte ptr [ebp+FFFFF7FAh],51h
05 004239A7 mov byte ptr [ebp+FFFFF7F4h],52h
06 004239AE mov byte ptr [ebp+FFFFF7F9h],52h
07 004239B5 mov eax,dword ptr [ebp+FFFFF7F0h]
08 004239BB mov ecx,dword ptr [eax+4]
09 004239BE mov byte ptr [ebp+ecx+FFFFF7F4h],53h
10 004239C6 mov eax,dword ptr [ebp+FFFFF7F0h]
11 004239CC mov ecx,dword ptr [eax+4]
12 004239CF lea ecx,[ebp+ecx+FFFFF7F0h]
13 004239D6 call 0041DF32
The first three rows are object initialization and the object constructor is called. Rows 4, 5, and 6 assign values to member variables of child classes and left and right parent classes. We can see that it is written directly, because the inheritance of this layer is normal inheritance. Rows 7th, 8, and 9 are values assigned to the grandfather class member variables. As discussed in the previous article, they are indirectly accessed through the offset value pointed to by the offset value pointer. The last four lines of commands call member functions. We can see that the called function address is provided directly (the last line), because we call the function through an object, even if it is a virtualFunction callThere will be no polymorphism. However, the method for getting the this pointer is indirect, that is, rows 10th, 11, and 12. Because this function is defined in the grandfather class, the data members it operates on should be grandfather class. Therefore, the compiler must adjust the position of this pointer. The grandfather class is inherited by virtual means, so it must be adjusted by the offset value pointed to by the offset value pointer.
Then, let's look at rows 9th and 12th. We can see that the calculated address values are different. This is because the 9th behavior assigns values to the member variables of the grandfather class, and the grandfather class has a virtual table pointer. Therefore, after obtaining the starting address of the object, the compiler adds a 4-byte offset to skip the virtual pointer. The actual calculation of the obtained address is: [ebp + ecx + FFFFF7F0h + 4 h]. The compiler will directly perform the last operation when generating the code.
Let's look at another example. The Inheritance structure of this example is the same as that in the previous article. It is also a diamond structure. The difference is that each class overwrites the top-level class declaration.Virtual Functions. The Code is as follows:
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
struct C140 : public virtual C041
{
C140() : c_(0x02) {}
virtual void foo() { c_ = 0x11; }
char c_;
};
struct C141 : public virtual C041
{
C141() : c_(0x03) {}
virtual void foo() { c_ = 0x12; }
char c_;
};
struct C150 : public C140, public C141
{
C150() : c_(0x04) {}
virtual void foo() { c_ = 0x21; }
char c_;
};
First, run the following code to check their memory layout.
PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C140)
PRINT_SIZE_DETAIL(C141)
PRINT_SIZE_DETAIL(C150)
Result:
The size of C041 is 5
The detail of C041 is f0 c2 45 00 01
The size of C140 is 14
The detail of C140 is 48 c3 45 00 02 00 00 00 00 44 c3 45 00 01
The size of C141 is 14
The detail of C141 is 58 c3 45 00 03 00 00 00 00 54 c3 45 00 01
The size of C150 is 20
The detail of C150 is 74 c3 45 00 02 68 c3 45 00 03 04 00 00 00 00 64 c3 45 00 01
Different from the previous layout, the sharing part and the previous non-sharing part have a 4-byte value of 0. Only the shared part has a virtual table pointer. This is because the derived classes do not define their own virtual functions, but only rewrite the virtual functions of the top-level classes. Let's analyze the C150 object layout.
|C140,5 |C141,5 |C150,1 |zero,4 |C041,5 |
|ospt,4,15 |m,1 |ospt,4,10 |m,1 |m,1 |4 |vtpt,4 |m1 |
(Note: I used the abbreviation to keep the rows straight. Ospt indicates the offset value pointer, m indicates the member variable, and vtpt indicates the virtual table pointer. The first number is the size of the region, that is, the number of bytes. Only the offset value pointer has a second number, and the second number is the size of the Offset Value pointed to by the offset value pointer .)
Let's look at the function call:
C150 obj;
PRINT_OBJ_ADR(obj)
obj.foo();
The output object address is:
objs address is : 0012F624
The assembly code corresponding to the code for the last function call is:
00423F74 lea ecx,[ebp+FFFFF757h]
00423F7A call 0041DCA3
After one step, we can see that the value of ecx is 0x0012F633, which is the starting address of the grandfather class part in the obj object layout. Through the above layout analysis, we know that the starting offset value pointer of C150 points to 15, that is, the offset value from the starting point of the object to the shared part (grandfather class part. After the start address of the obj output above is 0x0012F624 plus 15 in decimal format, it is exactly the value 0x0012f633 in ecx.
Since function calls act on objects, we can see that the call command of the second line is directed to the address.
The confusing problem here is that we know that ecx is used to pass the this pointer. In the previous article, we analyzed the foo method calls on the C110 object. In that example, because foo is a virtual function defined in the top-level class and is not overwritten by the following derived class, when you call this method through a subclass object, the code generated by the compiler is to calculate the starting address of the grandfather class by pointing to the offset value of the starting offset pointer of the subclass, and use this address as the address pointed to by this pointer. But in the C150 class, foo is no longer inherited from the grandfather class, but overwritten by the quilt class. In this case, the this pointer should point to the starting address of the subclass, that is, 0x0012F62E, instead of the value 0x0012F633 in ecx.
Let's take a look at the compilation code of C150: foo () and see how it locates the member variables of the subclass by pointing to the this pointer of the grandfather class.
01 00426C00 push ebp
02 00426C01 mov ebp,esp
03 00426C03 sub esp,0CCh
04 00426C09 push ebx
05 00426C0A push esi
06 00426C0B push edi
07 00426C0C push ecx
08 00426C0D lea edi,[ebp+FFFFFF34h]
09 00426C13 mov ecx,33h
10 00426C18 mov eax,0CCCCCCCCh
11 00426C1D rep stos dword ptr [edi]
12 00426C1F pop ecx
13 00426C20 mov dword ptr [ebp-8],ecx
14 00426C23 mov eax,dword ptr [ebp-8]
15 00426C26 mov byte ptr [eax-5],21h
16 00426C2A pop edi
17 00426C2B pop esi
18 00426C2C pop ebx
19 00426C2D mov esp,ebp
20 00426C2F pop ebp
21 00426C30 ret
Sure enough, because the Pointer Points to not the starting part of the Child class (but the starting part of the grandfather class) at this time, because it is by subtracting an offset value from