随笔- 7  文章- 0  评论- 16 

不保证本文是对滴,只是个人的理解,当然 大多是从书上看来的

using System;

class A

{

    private int i = 9;

    public void Say()

    {

        Console.WriteLine(i);

    }

}

class ts

{

    static void Main()

    {

        A a = new A();

        a.Say();        

    }

}

运行结果:9

一、 new A()的执行

(1)CLR统计A类和A类所有父类(到Object)的所有实例字段,在托管堆上分配相应大小的内存,将该内存的地址压入线程栈中。

(2)调用A类的构造函数,完成实例的初始化工作。

用VS2005+SOS看下对象实例在内存中的样子(蓝色为指令):

.load sos

已加载扩展C:"WINDOWS"Microsoft.NET"Framework"v2.0.50727"sos.dll

!clrstack -a

PDB symbol for mscorwks.dll not loaded

OS Thread Id: 0x6e8 (1768)

ESP       EIP     

0013f444 00f400b3 Test.Main()

    LOCALS:

        <CLR reg> = 0x013e1b64

0013f69c 79e88f63 [GCFrame: 0013f69c] 

这个0x013e1b64便是压入到线程栈中对象实例在堆中的地址。

顺着0x013e1b64找到堆上的对象实例:

!do 0x013e1b64

Name: A

MethodTable: 00a63088

EEClass: 00a613c0

Size: 12(0xc) bytes

 (E:"vs2005Pro"EditILTest"ConsoleApplication1"bin"Debug"ConsoleApplication1.exe)

Fields:

      MT    Field   Offset                 Type VT     Attr    Value Name

790fed1c  4000001        4         System.Int32  0 instance        9    i

         0x013e1b64

00000000  00a63088  00000009

》00000000 是同步块索引 ;  00a630880x013e1b64指向的内存,00a63088又指向A类方法表的地址;       00000009 是实例字段

A的方法表信息:

!dumpmt -md 00a63088

EEClass: 00a613c0

Module: 00a62c14

Name: A

mdToken: 02000002  (E:"vs2005Pro"EditILTest"ConsoleApplication1"bin"Debug"ConsoleApplication1.exe)

BaseSize: 0xc

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 6

--------------------------------------

MethodDesc Table

  Entry  MethodDesc      JIT Name

79354bec   7913bd48   PreJIT System.Object.ToString()

793539c0   7913bd50   PreJIT System.Object.Equals(System.Object)

793539b0   7913bd68   PreJIT System.Object.GetHashCode()

7934a4c0   7913bd70   PreJIT System.Object.Finalize()

00a630d0   00a63078      JIT A.Say()

00a630e0   00a63080      JIT A..ctor()

可以看到,A类一共有6个方法,4个虚方法继承自父类Object,一个定义的Say方法,和一个编译器自动生成的构造函数。

每个类在内存中对应一个方法表,方法表分为虚方法段、实例方法段,静态方法段,构造函数段。一个类的方法表包涵其所有父类的虚方法,而且排列顺序是固定的(程序每次运行可能不一样,但对于每次是固定的),也可称为:某一虚方法在继承层次中的位移值是不变的(多态的实现根本)

构造函数的调用过程:

如果一个类没有定义任何函数,编译时编译器便会生成一个默认构造函数。类的构造函数的完成都依赖于其直接父类构造函数的完成(除了Object)。当程序执行到构造函数时,首先要调用其父类的构造函数,再执行方法体内的代码。父类构造函数首先要调用其父类的构造函数,依次向上,直到Object(Object的构造函数啥也不干,仅仅返回)然后依次调用构造函数体的代码。但当声明字段时进行了初始化,那情况就不同了:先在构造函数中进行数据初始化,再调用父类的构造函数。

用个小例子看IL代码:

using System;

class A

{

    private int i; 

    public A()

    {

        i = 9;

    }

  

}   

class Test

{

    static void Main()

    {

        A a = new A();  

    }

}

A构造函数IL:

.method public hidebysig specialname rtspecialname 

        instance void  .ctor() cil managed

{

  // 代码大小       18 (0x12)

  .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()

  IL_0006:  nop

  IL_0007:  nop

  IL_0008:  ldarg.0

  IL_0009:  ldc.i4.s   9

  IL_000b:  stfld      int32 A::i

  IL_0010:  nop

  IL_0011:  ret

} // end of method A::.ctor

可以看出先调用父类构造函数(绿色代码),后初始化(红色代码)

把代码改成:

using System;

class A

{

    private int i = 9;  

    

}

class Test

{

    static void Main()

    {

        A a = new A();  

    }

}

A的构造函数IL:

.method public hidebysig specialname rtspecialname 

        instance void  .ctor() cil managed

{

  // 代码大小       16 (0x10)

  .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  ldc.i4.s   9

  IL_0003:  stfld      int32 A::i

  IL_0008:  ldarg.0

  IL_0009:  call       instance void [mscorlib]System.Object::.ctor()

  IL_000e:  nop

  IL_000f:  ret

} // end of method A::.ctor

可以看出:先初始化(绿色代码),后调用父类构造函数(红色)。

多态的实现原理:

多态的实现根本是依靠 虚方法在方法表中的布局规则

先从简单说起:

1、父类中的虚方法子类中都有,而且排列顺序是不变的(自己的理解)

例如:

using System;

class A{}

即使A类没有定义任何方法,它仍然有5个方法

MethodDesc Table

   Entry MethodDesc      JIT Name

79354bec   7913bd48   PreJIT System.Object.ToString()

793539c0   7913bd50   PreJIT System.Object.Equals(System.Object)

793539b0   7913bd68   PreJIT System.Object.GetHashCode()

7934a4c0   7913bd70   PreJIT System.Object.Finalize()

00a630b8   00a63068      JIT A..ctor()

其中4个从Object继承,一个是构造函数。

Object得方法表啥样呢:

MethodDesc Table

   Entry MethodDesc      JIT Name

79354bec   7913bd48   PreJIT System.Object.ToString()

793539c0   7913bd50   PreJIT System.Object.Equals(System.Object)

793539b0   7913bd68   PreJIT System.Object.GetHashCode()

7934a4c0   7913bd70   PreJIT System.Object.Finalize()

7934a4a8   7913bd40   PreJIT System.Object..ctor()

79eefc57   7913bd10    FCALL System.Object.InternalEquals(System.Object, System.Object)

79354c04   7913bd58   PreJIT System.Object.Equals(System.Object, System.Object)

79354c2c   7913bd60   PreJIT System.Object.ReferenceEquals(System.Object, System.Object)

79eeee5b   7913bd18    FCALL System.Object.InternalGetHashCode(System.Object)

79690ccc   7913bd20    FCALL System.Object.GetType()

79690ce0   7913bd28    FCALL System.Object.MemberwiseClone()

79690cf4   7913bd78   PreJIT System.Object.FieldSetter(System.String, System.String, System.Object)

79690d08   7913bd80   PreJIT System.Object.FieldGetter(System.String, System.String, System.Object ByRef)

79690d1c   7913bd88   PreJIT System.Object.GetFieldInfo(System.String, System.String)

由此得出两点:

1 父类的虚方法子类都有,而且排列顺序不变(也可叫方法的槽值不变,或者叫方法的相对位移不变)。

2 父类的非虚实例方法,静态方法子类没有

为方便说明,用下面例子,先不考虑Object的虚方法

using System;

class A

{  

public virtual void Say()   {  Console.WriteLine("In A"); }

}

class B:A

{  

public override void Say() {  Console.WriteLine("In B"); }

}

class C:A

 public override  void Say() { Console.WriteLine("In C"); }

}

class Test

{

    static void Main()

    {

        A a = new B();

        a.Say();        

    }

}

输出结果不用怀疑,刚学C#时候,让如此理解:A的方法是虚方法,可被子类覆盖,A a = new B()因为B类覆盖了A的虚方法,所以a.Say()调用的是B类的方法,同理A a = new C()也这么解释。这样理解显然不够,常常被继承和virtual,override弄迷糊,比如B里的Say不是override而是virtual,或者就是个实例方法,或者。。(面试题的由来)。不了解实现原理,很难确切搞清楚。

先看看a.Say()做了些什么:

   a.Say();

00000039  mov         ecx,edi   //将this指针装到ecx寄存器

0000003b  mov         eax,dword ptr [ecx]  //找到typehadle装到eax寄存器

0000003d  call        dword ptr [eax+38h]   // call方法 方法的地址在typehandle加上38h

用ws2005+sos看看:

先找到this:

!clrstack -a

PDB symbol for mscorwks.dll not loaded

OS Thread Id: 0xb0 (176)

ESP       EIP     

0013f444 00f400b1 Test.Main()

    LOCALS:

        <CLR reg> = 0x013e1b64

0013f69c 79e88f63 [GCFrame: 0013f69c]

由this找到typehandle,typehandle指向方法表:

!do 013e1b64

Name: B

MethodTable: 00a63118

EEClass: 00a61458

Size: 12(0xc) bytes

 (E:"vs2005Pro"EditILTest"ConsoleApplication1"bin"Debug"ConsoleApplication1.exe)

Fields:

None

call        dword ptr [eax+38h] 实际上是 call        dword ptr [00a63118+38h] 就是

call        dword ptr [00a63150]

通过内存窗口可看到00a63150存放着00a63160

call        dword ptr [00a63150] 就是 call 00a63160 (汇编很不熟 希望没说错)

00a63160又是什么:

!dumpmt -md 00a63118

EEClass: 00a61458

Module: 00a62c14

Name: B

mdToken: 02000003  (E:"vs2005Pro"EditILTest"ConsoleApplication1"bin"Debug"ConsoleApplication1.exe)

BaseSize: 0xc

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 6

--------------------------------------

MethodDesc Table

   Entry MethodDesc      JIT Name

79354bec   7913bd48   PreJIT System.Object.ToString()

793539c0   7913bd50   PreJIT System.Object.Equals(System.Object)

793539b0   7913bd68   PreJIT System.Object.GetHashCode()

7934a4c0   7913bd70   PreJIT System.Object.Finalize()

00a63160   00a63108      JIT B.Say()

00a63170   00a63110      JIT B..ctor()

call 00a63160看见没,红色的,是B.Say()的入口。

问题的关键在这三行:

00000039  mov         ecx,edi 

0000003b  mov         eax,dword ptr [ecx] 

0000003d  call        dword ptr [eax+38h] 

eax+38h】 这个38h就是方法在方法表中的位移,clr通过保证同一虚方法在继承层次结构中的方法位移是不变的,来实现了多态。

A a = new B();a.Say() a的类型是A(实际值是B),所以只有A类的方法对a是可见的,那为什么会调用B的方法呢。编译的时候,只看a的声明类型,a.Say()是虚方法调用(存在多态问题),需要在运行时真正的类型才能确定,但编译时就知道了虚方法Say的方法表位移(由A得到),因为虚方法在继承层次中位移是不变的,就在运行时通过this(运行时决定,编译时不知)加上固定的位移值(编译时可知)实现了多态。

-_-!真费劲)说白了:a.Say(),一看a是A类型,Say是虚方法,生成call [this+位移]的调用。运行时,A a = new B(),this指向B的实例的地址,this找到typehandle,typehandle加上固定位移,就找到了B的方法,同理A a = new C()

this指向C的实例的地址,this找到typehandle,typehandle加上固定位移,就找到了C的方法,关键就在A B C的方法Say的位移是一样的。

上面只说了一半(啰嗦半天),还有一重要机制:方法表布局过程。

有了布局保证,多态才能实现。

先看虚方法的几个修饰符 virtual override,它们两个其实分别对应相应的元数据属性virtual对应virtual和newslot;override对应virtual。

Newslot表示方法占用新的方法槽(方法位移),virtual表示方法是虚方法。

以下为翻译:对于特定的虚方法,每次程序运行,方法位移可能不一样,但程序运行的时候,这个值就是固定的(对于每次运行,位移这次可能是3,下次可能是4,但一旦运行,3或者4在这次运行中是不变的),方法表达位移值是在类型加载的时候计算的,计算的依据就是方法对应的元数据属性,其中对方法位移影响最大的就是newslot属性。

CLR为每个虚方法在方法表中分配一个槽,这个槽包含指向相应方法实现代码的指针,CLR认为带有newslot属性的虚方法跟父类的任何方法都没联系。CLR为带有newslot属性的虚方法分配一个新的槽,这个槽的位移比其父类的虚方法中最大的槽值至少大1.因为Object类,所有类的最终父类,已经包含了4个虚方法,显然任何类的方法表的前4个槽是留给Object的虚方法的,自己定义的虚方法只能往后排。如果虚方法没有带newslot属性,CLR就认为这个虚方法是其父类某个虚方法的替代,如果这样,CLR就去父类找和子类的这个虚方法名字和签名都相同的方法,如果找到了,子类中的这个虚方法就复用父类虚方法的槽,并将槽指向子类这个方法的自己的实现。因为当用父类引用调用方法时使用的是这个槽,所以对子类的调用就会被分派到子类的实现。

举个例子class A{}A中没有定义方法,就只是继承了Object的4个虚方法(占了前4个槽),并且每个槽都指向Object的相应方法的实现。所以,Object a = new A(),a.ToString()方法能够打印出A来。a.ToString()这个方法虽然被分排到了A方法表的相应槽,但这个槽却指向了父类(Object)的方法实现。说下:实例方法调用时,默认传递一个this参数,this指出了方法要操作对象的地址,拿a.ToString()来说,它实际执行的是Object的ToSting,为什么Object的ToSting打印出A而不打印别的呢,就在于a.ToString()默认的传递了this,相当于a.ToString(a)。换成class B{}Object b = new B;b.ToString()相当于b.ToString(b),虽然还是调用Object的ToString,可这次传递的this指向了B的实例。(啰嗦了)

如果:class A

{

    public override string ToString(){Console.Write("A's Tostring");}

}

Object a = new A();a.ToString();

当布局方法表的时候,CLR看到一个带override(对应的元数据属性是virtual,没有newslot)的ToString,就从Object中找名字叫ToString的,不带参数,返回值为string的方法(名称相同,签名相同),Object明显有一个,那么A的ToString就重用Object的ToString方法的槽,但将槽里存的地址指向了A自己的实现(Console.Write("A's Tostring");),所以调用a.ToString()结果是A's ToString.

虚方法的槽是由声明类型决定的:

using System;

class A

{

   public virtual string ToString(){

       return "KKK";

   }

}

在布局这个方法表时,ToString的修饰符是virtual,相当于元数据属性的virtual+newslot(表明这是个虚方法,而且和父类的任何方法都没有联系,所以直接分配新槽),所以实际上A里面有两个ToString,一个是Object的,占一个槽,实现指向Object的实现,另一个ToString占用新槽,实现指向自己的实现(KKK)。下面很关键:

static void Main()

    {

        Object a = new A();

        Console.WriteLine(a.ToString());

}

这样写,结果是"A". 因为a的声明类型是Object,所以ToString方法的槽值由Object决定,比方说是3,实际调用时,方法调用被分派到A,A的槽为3的方法就是继承自Object的那个,实现指向Object的实现(同一虚方法在继承层次中槽值是固定的),所以结果是A。

如果:

static void Main()

    {

        A a = new A();

        Console.WriteLine(a.ToString());

}

结果是:KKK

ToString方法的槽是由A决定的,可A里面有两个ToString,一个Object的(指向Object实现),一个自己的(指向自己的实现)。在这种情况下,ToString槽值取A自己的那个,看起来像是’隐藏‘了父类的ToString,所以代码编译时提示A的ToString隐藏了Object的ToString(但不报错,程序可执行),并且建议你将A中的ToString加上New,其实这个New加不加一样,只是为了让你明确自己在做什么,概念上实现了’隐藏‘。

A里有两个ToStringA a = new A();调用时为啥掉你A的ToString,不掉用Object,总得有个说法吧,说法有了,叫’隐藏‘ 嘎嘎。

总结:通过类加载时方法表布局规则,和调用时this的动态决定,以及固定的虚方法槽值,实现了多态。

再看看:

00000039  mov         ecx,edi 

0000003b  mov         eax,dword ptr [ecx] 

0000003d  call        dword ptr [eax+38h]

是不是明白了

上面用的那些指令 我也只知皮毛,能看到想看的东西就是了,以后再深入学习。

 posted on 2008-09-09 09:27  红泥  阅读(...)  评论(... 编辑 收藏