编译环境:win10;x64;debug struct Base { int base=0; virtual void Print() { }; virtual void Test() { }; virtual void Test1() { }; virtual void Test2() { }; virtual void Test3() { }; virtual void Test4() { }; virtual void Test5() { }; virtual void Test6() { }; virtual void Test7() { }; virtual void Test8() { }; virtual void Test9() { }; }; struct Son:Base { int son=0; void Print() { }; void Test() { }; void Test1() { }; void Test2() { }; void Test3() { }; void Test4() { }; void Test5() { }; void Test6() { }; void Test7() { }; void Test8() { }; void Test9() { }; }; int main() { Base* p = new Son(); p->base = 10; p->Print(); p->Test(); p->Test1(); p->Test2(); p->Test3(); p->Test4(); p->Test5(); p->Test6(); p->Test7(); p->Test8(); p->Test9(); cout << sizeof(p); }
多态:一个对象的不同表现形式。多态的实现关键在于父类的虚函数被子类重写,当使用父类指针去存储子类对象时会根据虚表调用重写的方法。在C++子类可以不使用关键字override,依然可以实现重写。
cout << sizeof(p);最后打印出的结果是24,son类自身有一个字段,继承了父类一个字段,那还有八个字节是什么?
小篮圈是该对象的首地址 ,目前我们不知道是什么,在new一个对象的时候会向堆申请sizeof(son)个字节的内存,后面的两个字段已经被归0,这就是son对象的内存分布。(new 对象的时候后面加括号,会帮我们把字段归0)
当我们给base字段赋值完成后,第一个字段就变成了0a 00..,这说明从父类继承发字段在自身字段之前。
当我们总结出内存分布后,我们去未知的内存里看一下哪里有什么?
这是一段连续的内存,里面存了不知道的指向哪里的内存?这里我们先不管,继续执行。
p->Print(); //从p指针取八位,存入rax寄存器,这里取出了son对象的地址 [p]=new Son(); rax=son对象地址 00007FF6BA13251E mov rax,qword ptr [p] //从寄存器rax取八位存入rax rax=son的首地址=对象的第一条未知地址 00007FF6BA132522 mov rax,qword ptr [rax] //把son对象地址写入rcx寄存器 00007FF6BA132525 mov rcx,qword ptr [p] //相当于调用了rax寄存器指向的地址 9f 11 13 ba f6 7f 00 00 ==0x7ff6ba13119f 00007FF6BA132529 call qword ptr [rax]
我们继续跟进
这里已经告诉我们了 0x7ff6ba13119f 这条地址指向的是son的Print方法。 Son::Print (07FF6BA132050h) 到这里我们已经知道,son对象的第一条地址是son对象的地址。我们继续跟进
p->Print(); //从p指针取八位,存入rax寄存器,这里取出了son对象的地址 [p]=new Son(); rax=son对象地址 00007FF6BA13251E mov rax,qword ptr [p] //从寄存器rax取八位存入rax rax=son的首地址=对象的第一条未知地址 00007FF6BA132522 mov rax,qword ptr [rax] //把son对象地址写入rcx寄存器 00007FF6BA132525 mov rcx,qword ptr [p] //相当于调用了rax寄存器指向的地址 9f 11 13 ba f6 7f 00 00 ==0x7ff6ba13119f 00007FF6BA132529 call qword ptr [rax]
从我们上面的汇编代码可以看出,和在函数内部执行来看,rcx的地址就是son对象的地址,也就是说在执行函数的时候,把对象的地址也传入进来了。接下来看下一个虚方法的执行
69: p->Test(); //流程同上 00007FF6BA13252B mov rax,qword ptr [p] 00007FF6BA13252F mov rax,qword ptr [rax] 00007FF6BA132532 mov rcx,qword ptr [p] //这里地址偏移了八位 0x7ff6ba13150f 00007FF6BA132536 call qword ptr [rax+8]
第二个地址和test方法的地址相同,然后我们去对比一下两个蓝色区域的地址,右边蓝色区域的地址等于左边蓝色区域执行跳转语句的地址。
到这里我们已经知道了,son对象的一个未知地址是son对象第一个方法跳转语句的地址,这个就是虚方法被重写后能够被正确调用的真相。我这里有11个虚函数,结果只占用八个字节,因为永远只会存首个重写方法的地址,右边蓝色区域的地址也被称为虚表。
在对象刚创建出来的时候内存被cd填充。
当跳转到这一步的时候,内存会被0填充。
调用son对象构造函数的时候,虚表会被创建出来。son类在没有构造函数的时候,生成了默认构造,并调用了。(并不是所有没有构造函数的类都会生成并调用构造函数)