In a twinkling of an eye, it has been a long time since the end of the [C/C ++ language entry] serialization. I have been busy with my graduation thesis and work tasks recently, and I am working on a very important task. So my mind has been in a tight state recently and I find my brain cells are not enough. Come on!
Today, a friend asked me a question, that is, in the case of the Multi-inheritance of c ++, where the diamond Inheritance occurs, there is a problem with the memory distribution during Object Construction and the calling process of the constructor. After explaining it to him, I feel it is necessary to write down the process. If you have anything wrong, please give us your valuable comments. Thank you, at the same time, if you know this, you can skip this article.
Well, let's start with the topic. The simplest structure of the so-called diamond inheritance is as follows:
Class
{
Public:
A (void): nvar (0xaaaa0000 ){}
Public:
Int nvar;
};
Class B1: public
{
Public:
B1 (void ){}
};
Class B2: public
{
Public:
B2 (void ){}
};
Class C: Public B1, public B2
{
Public:
C (void ){}
};
It is such a multi-inheritance that graphically represents the relationship between them:
A
//
//
B1 B2
//
//
C
Then, in the create C object:
Int main (void)
{
C OBJ;
Return 0;
}
I think you should know what this will cause. Here we can clearly know the size of obj is 8, why is it 8, first look at the memory distribution:
Assume that the OBJ memory address is 0x0012ff18.
0x0012ff18:00 00 aa 00 00 aa
After reading the memory of the obj object, there are two copies of A. The red one is the memory inherited from the line B1, and the blue one is inherited from the line B2. Therefore, the constructor of A is called twice. Here B1 is in the front, and B2 is in the back because one-to-many inheritance is distributed from left to right.
It is obvious from here that such an ending is a tragedy. What's even worse is that if you use obj to access nVar members, compilation errors will occur:
OBJ. nvar = 0x100;
Access to nVar is unclear because there are two replicas, And the compiler does not know which replicas you want to modify, resulting in compilation errors. It is also true to access member functions here.
So, is there any solution to prevent this phenomenon? C ++ proposed virtual inheritance to solve this problem:
Class
{
Public:
A (void): nVar (0xaaaa0000 ){}
Public:
Int nVar;
};
Class B1:VirtualPublic
{
Public:
B1 (void ){}
};
Class B2:VirtualPublic
{
Public:
B2 (void ){}
};
Class C: Public B1, public B2
{
Public:
C (void ){}
};
After the inheritance, a retains only one copy, and then looks at the memory distribution (Here we declare that I use the vc2008 version for testing ):
Assume that the OBJ memory address is 0x0012ff10.
0x0012ff10:0036680c 00415800 Aaaa0000
It can be clearly seen that there is only one 0xaaaa0000 here, and there are two more values in front, the size of obj is 12 bytes, and the blue address in front is the virtual base pointer (vbtable) of Class C) if A has a virtual function, a virtual function table (vftable) is added between blue and red, which occupies 16 bytes. The memory distribution of multiple inherited virtual tables is not described here.
Well, the following is the focus of this Article. Let's take a look at the process of calling the constructor when the OBJ object is created:
The process is probably: When obj is created, Class C constructor will be called first. In the constructor, the offset of the two vbtables will be assigned to the previous blue part of memory. Then the constructor of A is called, and the constructor of B1 and B2 is called.
Use pseudo code:
C ()
{
Vbtable;
Vbtable;
A: ();
B1: B1 ();
B2: B2 ();
}
So when we call the constructor B1 and B2, we will call the constructor A. Because B1 and B2 inherit from a, why didn't we call the constructor? Let's look at the disassembly code:
First, let's look at the main function:
C OBJ;
004113de Push 1
004113e0 Lea ECx, [OBJ]
004113e3 call C: C (4110e6h)
Call the constructor C in red, and then look at the constructor C:
00411460 push EBP
00411461 mov EBP, ESP
00411463 sub ESP, 0cch
00411469 push EBX
0041146a push ESI
0041146b push EDI
0041146c push ECx
0041146d Lea EDI, [ebp-0CCh]
00411473 mov ECx, 33 H
00411478 mov eax, 0 cccccccch
0041147d rep STOs dword ptr es: [EDI]
0041147f pop ECx
00411480 mov dword ptr [ebp-8], ECx
00411483 cmp dword ptr [EBP + 8], 0
00411487 je C: C + 47 h (4114a7h)
00411489 mov eax, dword ptr [this]
0041148c mov dword ptr [eax], offset C: 'vbtable' (201780ch)
00411492 mov eax, dword ptr [this]
00411495 mov dword ptr [eax + 4], offset C: 'vbtable' (415800 H)
0041149c mov ECx, dword ptr [this]
0041149f add ECx, 8
004114a2 call A: A (4110ebh)
004114a7Push 0
004114a9MoV ECx, dword ptr [this]
004114acCall B2: B2 (4110aah)
004114b1Push 0
004114b3MoV ECx, dword ptr [this]
004114b6Add ECx, 4
004114b9Call B1: B1 (41107dh)
004114be mov eax, dword ptr [this]
004114c1 pop EDI
004114c2 pop ESI
004114c3 pop EBX
004114c4 add ESP, 0cch
004114ca cmp ebp, ESP
004114cc call @ ILT + 330 (_ rtc_checkesp) (41114fh)
004114d1 mov ESP, EBP
004114d3 pop EBP
004114d4 RET 4
The blue text above indicates the bold font. You can see that the vbtable value is assigned. The bold Section in red below calls the constructor of. This is not surprising.
Before calling the constructor of A, there is a sentence: Add ECx, 8 to locate this to two vbtables. when calling the constructor of, write the value directly to the memory address pointed to by this: 0xaaaa0000. Therefore, the layout is formed:
0x0012ff10:0036680c00415800Aaaa0000
C: This/(vbtable) vbtable A: This
This of c is, of course, 0x0012ff10. This of a is 0x0012ff18, which is separated by two vbtables. In fact, this is the starting address of a class and there is nothing special about it.
Here, you may notice that the two identical commands push 0 in blue bold and red bold are obviously added by the compiler, And the B2 constructor obviously has no parameters, in this way, pushing a 0 is a bit similar to an implicit parameter. So what did we do when pushing a 0? Let's look at the B1 constructor:
00411550 push EBP
00411551 mov EBP, ESP
00411553 sub ESP, 0cch
00411559 push EBX
0041155a push ESI
0041155b push EDI
0041155c push ECx
0041155d Lea EDI, [ebp-0CCh]
00411563 mov ECx, 33 H
00411568 mov eax, 0 cccccccch
0041156d rep STOs dword ptr es: [EDI]
0041156f pop ECx
00411570 mov dword ptr [ebp-8], ECx
00411573Cmp dword ptr [EBP + 8], 0
00411577 je B1: B1 + 3DH (41158dh)
00411579 mov eax, dword ptr [this]
0041157c mov dword ptr [eax], offset B1: 'vbtable' (415818 H)
00411582 mov ECx, dword ptr [this]
00411585 add ECx, 4
00411588 call A: A (4110ebh)
0041158dMoV eax, dword ptr [this]
00411590 pop EDI
00411591 pop ESI
00411592 pop EBX
00411593 add ESP, 0cch
00411599 cmp ebp, ESP
0041159b call @ ILT + 330 (_ rtc_checkesp) (41114fh)
004115a0 mov ESP, EBP
004115a2 pop EBP
004115a3 RET 4
The instruction in the red sentence is obvious. EBP + 8 is the first parameter of the function. Although it does not exist here, it is pushed into a 0, so that a CMP is equal to 0, execute the blue jump and directly jump through the constructor to call the green command. In this way, the constructor of A is called only once. The constructor of B2.
With this push 0 operation, check whether it is zero. Therefore, even if you show that you are calling the constructor of A in B1 and B2, the result will not call the constructor of.
For example, B1 (void): A () {} jumps directly to the user code of the constructor because it is null.
Well, this article is almost the same here. Here we only introduce the principle of constructing function calls in virtual inheritance. I hope you will give more comments.