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

继承与多态

个人理解,欢迎讨论

using System;

public abstract class Animal

{

    public string name = "Animal";

    public abstract void ShowType();

}

public class Bird : Animal

{

    public string name = "Bird";

    public override void ShowType()

    {

        Console.WriteLine("Name is {0}", name);

    }

}

public class TestInheritance

{

    public static void Main()

    {

        Animal animal = new Bird();

        Console.WriteLine("Name value is {0}", animal.name);

        animal.ShowType();

        Console.ReadKey();

    }

}

看到上面小例子,或许你难以说出答案,或许你知道输出是什么,但你有没有这样的疑问:

1、Bird实例中到底有几个name呢,如果有两个,它们是如何区分的,关系又是怎样的?

2、animal.nameanimal.ShowType()为何会输出‘Animal,Name is Bird’的结果呢。
我们关心的不是输出是什么,而是为什么会有那样的输出。

上面的问题归根结底是继承和多态如何实现的问题。了解了继承和多态的实现原理,上面的问题和疑惑就不复存在了。

先说继承,子继承父,看看子类和父类都有什么,继承便更好了解了。

用俺仅会的几个指令剖开父子看看(蓝色为指令):

父类Animal:

!dumpclass 00af1368

Class Name: Animal

mdToken: 02000002 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)

Parent Class: 790c3ef0

Module: 00af2c5c

Method Table: 00af30a0

Vtable Slots: 5

Total Method Slots: 6

Class Attributes: 100081  Abstract, 

NumInstanceFields: 1

NumStaticFields: 0

      MT    Field   Offset                 Type VT     Attr    Value Name

793308ec  4000001        4        System.String  0 instance           name

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

Parent Class: 790c3ef0----- Animal的父类,是谁?

!dumpclass 790c3ef0

Class Name: System.Object

mdToken: 02000002 (C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)

Parent Class: 00000000

Module: 790c1000

Method Table: 79330508

Vtable Slots: 4

Total Method Slots: a

Class Attributes: 102001  

NumInstanceFields: 0

NumStaticFields: 0

根,本不用介绍

Method Table: 00af30a0------Animal方法表的地址,由此能看到Animal所有的方法

!dumpmt -md 00af30a0

EEClass: 00af1368

Module: 00af2c5c

Name: Animal

mdToken: 02000002  (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)

BaseSize: 0xc

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 6

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

MethodDesc Table

   Entry MethodDesc      JIT Name

79286a70   79104934   PreJIT System.Object.ToString()

79286a90   7910493c   PreJIT System.Object.Equals(System.Object)

79286b00   7910496c   PreJIT System.Object.GetHashCode()

792f72f0   79104990   PreJIT System.Object.Finalize()

00afc030   00af307c     NONE Animal.ShowType()

00afc038   00af3088      JIT Animal..ctor()

Vtable Slots: 5-----Animal方法表中虚方法的个数--5个(上面红的),前4个是从Object继承来的4个虚方法,ShowType是自定义的虚方法。

Total Method Slots: 6----Animal所有方法个数(上面5个虚的加上构造函数)

Class Attributes: 100081  Abstract,-----类属性,Abstract指明为抽象类

NumInstanceFields: 1----实例字段个数(1个),就是那个name

      MT    Field   Offset           Type  VT     Attr    Value Name

793308ec  4000001       4    System.String  0      instance      name----实例字段name

再者有必要注意的是Animal的构造函数:

  

.method hidebysig specialname rtspecialname instance void  .ctor() cil managed

{

    .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  ldstr      "Animal"

  IL_0006:  stfld      string Animal::name

  IL_000b:  ldarg.0

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

  IL_0011:  nop

  IL_0012:  ret

}

如上,Animal被解剖了,有几个问题需要思考一下:

1、Animal为抽象类,不能被实例化,那构造函数(Animal..ctor())有何用?

2、既然不可能存在Animal类的实例,那实例字段name有什么用处?

3、实例字段name的OffSet为4是什么意思?

问题先留着,接着看Bird:

Bird的类型信息:

!dumpclass 00af13cc

Class Name: Bird

mdToken: 02000003 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)

Parent Class: 00af1368

Module: 00af2c5c

Method Table: 00af3130

Vtable Slots: 5

Total Method Slots: 6

Class Attributes: 100001  

NumInstanceFields: 2

NumStaticFields: 0

      MT    Field   Offset                 Type VT     Attr    Value Name

793308ec  4000001        4        System.String  0 instance           name

793308ec  4000002        8        System.String  0 instance           name

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

由上可看出:Bird的父类是Animal(Parent Class: 00af1368),有5个虚方法(Vtable Slots: 5),一共有6个方法(Total Method Slots: 6),有2个实例字段(NumInstanceFields: 2),一个字段叫name,位移为4,一个字段也叫name,位移为8。

Bird的方法表信息:

!dumpmt -md 00af3130

EEClass: 00af13cc

Module: 00af2c5c

Name: Bird

mdToken: 02000003  (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)

BaseSize: 0x10

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 6

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

MethodDesc Table

   Entry MethodDesc      JIT Name

79286a70   79104934   PreJIT System.Object.ToString()

79286a90   7910493c   PreJIT System.Object.Equals(System.Object)

79286b00   7910496c   PreJIT System.Object.GetHashCode()

792f72f0   79104990   PreJIT System.Object.Finalize()

00afc050   00af3104      JIT Bird.ShowType()

00afc058   00af3110      JIT Bird..ctor()

再看Bird的构造函数:

.method  hidebysig specialname rtspecialname instance void  .ctor() cil managed

{

  .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  ldstr      "Bird"

  IL_0006:  stfld      string Bird::name

  IL_000b:  ldarg.0

  IL_000c:  call       instance void Animal::.ctor()

  IL_0011:  nop

  IL_0012:  ret

}

问题又来了:

1、Bird有两个实例字段name,一个是Bird本身的(public string name = "Bird";),另一个是从哪来的?(不难猜到,是从Animal继承过来的),但哪个name是Animal的,哪个又是Bird的?

2、两个name,一个位移为4,一个位移为8,是啥意思?

3、既然实例字段name被Bird继承来了,那Animal的方法会被Bird继承么?

将问题汇总一下:

1、Animal为抽象类,不能被实例化,那构造函数(Animal..ctor())有何用?

2、既然不可能存在Animal类的实例,那实例字段name有什么用处?

3、实例字段name的OffSet为4是什么意思?

4、Bird有两个实例字段name,一个是Bird本身的(public string name = "Bird";),另一个是从哪来的?(不难猜到,是从Animal继承过来的),但哪个name是Animal的,哪个又是Bird的?

5、两个name,一个位移为4,一个位移为8,是啥意思?

6、既然实例字段name被Bird继承来了,那Animal的方法会被Bird继承么?

通过问题3 4 5,大概能猜出来第一个name(位移为4)是Bird继承自Animal的,第二个name(位移为8)的是Bird自己的,猜对么?验证一下 找来Bird的实例看看:

!do 0x00c73528

Name: Bird

MethodTable: 00af3130

EEClass: 00af13cc

Size: 16(0x10) bytes

 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)

Fields:

      MT    Field   Offset                 Type VT     Attr    Value Name

793308ec  4000001        4        System.String  0 instance 00c73554 name

793308ec  4000002        8        System.String  0 instance 00c73538 name

可以看到Bird实例有两个name,而且Value也不一样:

!do -nofields 00c73554

Name: System.String

MethodTable: 793308ec

EEClass: 790ed64c

Size: 30(0x1e) bytes

 (C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)

String: Animal

!do -nofields 00c73538

Name: System.String

MethodTable: 793308ec

EEClass: 790ed64c

Size: 26(0x1a) bytes

 (C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)

String: Bird

知道结果了,显然不够,还要知道过程:两个name是怎么被分别整成‘Animal’和‘Bird’的。

这要从对象的实例化说起,一切源于new Bird()。

new Bird()是怎样一个过程:

1、CLR计算并分配对象所需空间,包括:同步块索引,方法表地址,实例字段及其他一些信息,如下图:


并将所分配空间的地址压栈(ObjectInstance),在此过程,CLR会为实例字段赋默认值(值类型为0,引用类型为NULL)。

2、调用构造函数对实例字段进行初始化

过程2可通过Bird构造函数的IL代码粗略了解:

.method  hidebysig specialname rtspecialname instance void  .ctor() cil managed

{

  .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  ldstr      "Bird"                 将‘Bird’的地址入栈

  IL_0006:  stfld      string Bird::name      将栈上‘Bird’地址存入name字段

  IL_000b:  ldarg.0

  IL_000c:  call       instance void Animal::.ctor()

  IL_0011:  nop

  IL_0012:  ret

}

首先初始化字段name(绿色),再调用父类Animal的构造函数。

或许ldarg.0,Bird::name还是不够’真实‘,那看看它们对应的汇编:

说明:汇编我不懂,所以以下纯属瞎猜,不保证对

!u 00af3110

Normal JIT generated code

Bird..ctor()

Begin 003b00f8, size 47

003B00F8 55               push        ebp

003B00F9 8BEC             mov         ebp,esp

003B00FB 57               push        edi

003B00FC 56               push        esi

003B00FD 53               push        ebx

003B00FE 83EC30           sub         esp,30h

003B0101 33C0             xor         eax,eax

003B0103 8945F0           mov         dword ptr [ebp-10h],eax

003B0106 33C0             xor         eax,eax

003B0108 8945E4           mov         dword ptr [ebp-1Ch],eax

003B010B 894DC4           mov         dword ptr [ebp-3Ch],ecx

003B010E 833D142EAF0000   cmp         dword ptr ds:[00AF2E14h],0

003B0115 7405             je          003B011C

003B0117 E865A3D179     call        7A0CA481 (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)

003B011C 8B053420C701     mov         eax,dword ptr ds:[01C72034h] ("Bird")

003B0122 8B4DC4           mov         ecx,dword ptr [ebp-3Ch]

003B0125 8D5108           lea         edx,[ecx+8]

003B0128 E8632CAC79       call        79E72D90 (JitHelp: CORINFO_HELP_ASSIGN_REF_EAX)

003B012D 8B4DC4           mov         ecx,dword ptr [ebp-3Ch]

003B0130 E803BF7400       call        00AFC038 (Animal..ctor(), mdToken: 06000002)

003B0135 90               nop

003B0136 90               nop

003B0137 8D65F4           lea         esp,[ebp-0Ch]

003B013A 5B               pop         ebx

003B013B 5E               pop         esi

003B013C 5F               pop         edi

003B013D 5D               pop         ebp

003B013E C3               ret

通过看上面红色的指令,应该是将Bird的地址存入了[ecx+8]位置。

验证一下:

我们看看这个地址到底是指哪:

mov         ecx,dword ptr [ebp-3Ch]    ebp的值是0012F480,这条指令结果是将00c73528放入寄存器ecx中,00c73528又是什么呢?

!do 00c73528

Name: Bird

MethodTable: 00af3130

EEClass: 00af13cc

Size: 16(0x10) bytes

 (C:"Documents and Settings"Administrator"My Documents"Visual Studio 2005"Projects"ph"ph"bin"Debug"ph.exe)

Fields:

      MT    Field   Offset                 Type VT     Attr    Value Name

793308ec  4000001        4        System.String  0 instance 00c73554 name

793308ec  4000002        8        System.String  0 instance 00c73538 name

示意图大概:


00c73528再加上8([ecx+8]得00c73530,地址00c73530存放着00c73538,看看00c73538又是什么:

!do -nofields 00c73538

Name: System.String

MethodTable: 793308ec

EEClass: 790ed64c

Size: 26(0x1a) bytes

 (C:"WINDOWS"assembly"GAC_32"mscorlib"2.0.0.0__b77a5c561934e089"mscorlib.dll)

String: Bird

终于找到了!!是Bird

这也就说明了上面红色汇编确实是把Bird存入了[ecx+8]位置,这个8就是字段的偏移量。

以上便是Bird构造函数中字段初始化部分,接着便调用父类Animal的构造函数:

.method hidebysig specialname rtspecialname instance void  .ctor() cil managed

{

    .maxstack  8

  IL_0000:  ldarg.0

  IL_0001:  ldstr      "Animal"

  IL_0006:  stfld      string Animal::name

  IL_000b:  ldarg.0

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

  IL_0011:  nop

  IL_0012:  ret

}

找到对应函数的汇编,取其中我们关心的部分:

003B0174 8B053820C701     mov         eax,dword ptr ds:[01C72038h] ("Animal")

003B017A 8B4DC4           mov         ecx,dword ptr [ebp-3Ch]

003B017D 8D5104           lea         edx,[ecx+4]

003B0180 E80B2CAC79       call        79E72D90 (JitHelp: CORINFO_HELP_ASSIGN_REF_EAX)

这次是吧’Animal‘存入‘[ecx+4]位置,所以上面那图在实例化完成后是这样的:


这下问题:

Animal为抽象类,不能被实例化,那构造函数(Animal..ctor())有何用?

既然不可能存在Animal类的实例,那实例字段name有什么用处?

两个问题似乎有答案了吧:

父类的构造函数负责初始化父类所定义的实例字段(在子类的构造函数中调用),在调用父类构造函数的时候,子类实例的地址是被当参数传递过去的,所以:

 IL_000b:  ldarg.0

 IL_000c:  call       instance void Animal::.ctor()

 ldarg.0正是Bird实例的地址,在Animal的构造函数中,拿Bird实例的地址加上Animal的实例字段的位移(4),完成了Animal定义的name的初始化工作。

所以事情大概是这样的(Bird构造函数执行过程):

ldarg.0

ldstr      "Bird"               

stfld      string Bird::name      

ldarg.0

call       instance void Animal::.ctor()

取得Bird实例的地址,加上位移8,完成了Bird定义name的初始化,接着调用Animal的构造函数,在Animal的构造函数中,取得Bird实例的地址(当参数传过来的),加上位移4,完成Animal定义name的初始化,而位移值(4,8)是在CLR为实例分配空间的时候就规定好的,以后在读取字段时,只需要知道该用哪个位移值就行了。

Animal animal = new Bird();

Console.WriteLine("Name value is {0}", animal.name);这个animal.name会取出哪个name呢,关键是编译器按既定的规则取哪个位移值了。

上面的关键汇编是:00000052  mov         edx,dword ptr [eax+4] 

可见用的位移值4去取相应字段了,至于是取出哪个值,就看编译器的编译规则和CLR如何安排位移了。

我的意思是,对于计算机而言,初始化时CLR把两个字段的位移设成4 和8 ,并把位移4处初始化为Animal,8处为Bird。编译器看到Animal animal = new Bird();Animal.name时,会用位移4去取值,为啥不用8,你问写编译器的吧,相应的,看到Bird animal = new Bird();Animal.name时,会用位移8去取值。对于我们而言,规则变成了:实例字段的取值是按其声明类型的,所以变量aminal的声明类型是哪个,就去哪个的值。

下面该说多态了:

就是animal.ShowType();为啥会打印这个,而不打印出别的的问题。偷个懒,就不再写了,我文章‘实例化与多态’里面有还算详细的胡侃,都看到这里了,估计你也不介意找下那篇了。


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