概述
先看下面的代码:
class A
{
public void T()
{
Console.WriteLine("A");
}
}
class B : A
{
public new void T()
{
Console.WriteLine("B");
}
}
A a1 = new A();
A a2 = new B();
B b1 = new B();
b b2 = (B)a2;
a1.T();
a2.T();
b1.T();
b2.T();
我们看看从实例化a1开始的汇编代码:
A a1 = new A();
mov ecx,8B96A0h --这个地址的含义,后面会说
call FCF90E64 --这行具体做了什么,我现在还不清楚,应该是调用构造函数前的准备工作
mov ebx,eax
mov ecx, ebx --这里保存的是对象a1的地址
call FCFA8530 --这里调用了构造函数,也就是A::A()
mov dword ptr [ebp-44h],ebx --这里把a1的地址保存到栈里面。可以看到,用的是ebp-44h,是做的减法,后面几个对象,减掉的数值会更大,正好符合栈是从高字节开始的说法。
A a2 = new B();
mov ecx,8B9728h --注意这个地址和上面的不一样了
call FCF90E64 --前两行还是一样的,没有变化
mov ebx,eax
mov ecx, ebx
call FCFA85C8 --调用B的构造函数B::B(),所以地址变了
mov esi,ebx --其实这里应该是保存到栈里的。但是因为后面很快要用,代码被进行了优化,就直接留在了寄存器里面
B b1 = new B();
mov ecx,8B9728h --这个地址跟上面的一样,看起来是跟类型相关的,也就是和B有关系
call FCF90E64
mov ebx,eax
mov ecx, ebx
call FCFA85C8 --调用B的构造函数B::B()
mov dword ptr [ebp-4Ch],ebx --保存到栈里的下一个位置。
b2的汇编代码我就不写了,有些乱,不过本质就是做了下类型转换,然后把结果保存到了ebp-50h
通过上面的代码,我们可以看到,现在程序里面有3个实例对象,它们生存在堆空间里面。另外还有4个指针,3个在栈里面,一个在寄存器里面。不过,当然不仅仅是这些的。其中有两个“东西”值得我们好好研究一下。
这两个东西就是在8B96A0h位置的A对象和在8B9728h位置的B对象。实际上,我认为,它们应该就是class A和class B的实例,或许我们称作它们为“类的类型的实例”更准确些,因为这两个实例的类型就是Type。这两个实例也在堆里面。当我们调用Type去获取A的信息的时候,其实操作的就是这个实例对象。并且,a1有一个指针就是指向8B96A0h位置的A对象的,表明自己是A类型的实例。而a2和b1也同样有这样的一个指针,指向B。同时,b2与a2有相同的值,只是b2声明的类型是B。
事实上,最开始的那两行汇编:
mov ecx,8B96A0h
call FCF90E64
做的事情,很可能就是保存指向A或者B对象的指针。
在继续往下走之前,先总结一下。
现在我们有3个普通的实例对象,2个特别的类实例对象,他们都在堆里面。然后栈里面有3个指针,寄存器里面有1个,这四个指针指向前面3个对象。
好了,现在继续:
a1.T();
mov ecx,dword ptr [ebp-44h] --取出a1
cmp dword ptr[ecx],ecx
call FCFA8540 --T方法的地址,调用了T方法。可以看到,这是一个固定的地址
nop
剩下的代码都类似,而且已经在上一篇说过了,这里就不说了。
下面看看对于virtual和override关键字,是什么样子的。我这里就直接使用上一篇里面的C和D了,代码也不变。
class C
{
public virtual void T()
{
Console.WriteLine("C");
}
}
class D : C
{
public override void T()
{
Console.WriteLine("D");
}
}
C c1 = new C();
C c2 = new D();
D d1 = new D();
D d2 = (D)c2;
c1.T();
c2.T();
d1.T();
d2.T();
下面直接看调用T方法部分的汇编内容。
c1.T():
mov ecx,dword ptr [ebp-5Ch] --先从栈里面取出c1,我这观察到的ecx是00C7A240
mov eax,dword ptr [ecx] --再从00C7A240位置取值,这个应该是C对象的地址,我这里是8B98E0
call dword ptr [eax+38h] --再偏移38h,那个内存里面指向的位置,就是真正的C的T方法的代码了。
nop
c2.T():
mov ecx,edi --这个是放在了寄存器里面而已,取出后是00C7A24C
mov eax,dword ptr [ecx] --再从00C7A24C位置取值,这个应该是D对象的地址,我这里是8B9978
call dword ptr [eax+38h] --再偏移38h,那个内存里面指向的位置,就是真正的D的T方法的代码了。
nop
d1.T():
mov ecx,dword ptr [ebp-64h] --从栈里取出d1,取出后是00C7A25C
mov eax,dword ptr [ecx] --再从00C7A25C位置取值,因为还是D对象,所以值还是8B9978
call dword ptr [eax+38h] --再偏移38h,那个内存里面指向的位置,就是真正的D的T方法的代码了。
nop
d2.T():
mov ecx,dword ptr [ebp-68h] --从栈里取出d2,因为和c2指向的是同一个实例,所以值一样,是00C7A24C
mov eax,dword ptr [ecx]
call dword ptr [eax+38h]
nop
从上面的代码和寄存器的值可以看出:
1:c2和d1确实是指向了两个不同的实例,不过因为这两个实例都是D类型的,所以计算出来的地址是一样的。这就说明,对于每一个实例,都有一个指向“类的类型的实例”的指针,来表明自己的身份。而“类的类型的实例”里面,则记录着每个方法所在的入口地址。需要说明的一点是,如果是第一次调用某个方法,这个入口地址应该是不存在的,JIT会把相应的IL代码编译成本地代码,然后让入口地址指向编译好的本地代码
2:通过c2和d2可以看出来,这个时候,类型已经没有什么作用了,最后调用的方法,不会因为c2和d2类型的不同而有区别。这一点,是和不使用virtual的方法的最大的区别。
最后,让我们来整理一下我们代码的执行流程。
1、从栈(或者寄存器)里面取出某个指针,比如c1
2、指针指向的对象在堆里面,具体的位置,就是指针指向的内存里面的值。根据这个值,找到堆里面的对象
3、根据对象的指向类型的指针,在堆里找到“类的类型的实例”,然后根据偏移地址,找到方法的入口地址
4、跳转到入口地址所指向的内存,执行代码。如果地址不存在,则JIT会编译IL代码成本地代码,然后修改入口地址为编译好的代码地址,然后系统再执行相应的代码
最后
以上就是殷勤灰狼为你收集整理的也谈.net下面的new、virtual和override(二)的全部内容,希望文章能够帮你解决也谈.net下面的new、virtual和override(二)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复