也谈.net下面的new、virtual和override(二)

来源:互联网 发布:maya软件培训 编辑:程序博客网 时间:2024/05/12 06:37

先看下面的代码:
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代码成本地代码,然后修改入口地址为编译好的代码地址,然后系统再执行相应的代码

 

原创粉丝点击