再探CLR对象模型

字段
一个类中可以有多个字段,我们可以在定义字段的同时对其进行初始化。如果子类和父类都有多个字段需要初始化,那么,其初始化的顺序是怎样的呢?
1.实例字段的初始化顺序
请看以下C#代码:
class Parent
{
    public int pD = 100;
}
class Child:Parent
{
    public int cD = 200;
}
class Program
{
    static void Main(string[] args)
    {
        Child c = new Child();
    }
}
使用ildasm工具查看Main()函数的IL代码:
.method private hidebysig static void  Main(string[] args) cil managed
{
    //……
    IL_0001:  newobj     instance void CSConsoleForTest.Child::.ctor()
    //……
} // end of method Program::Main
可以看到它直接调用Child类的默认构造函数。
Child类的构造函数的IL代码如下:
.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
  // 代码大小       19 (0x13)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4     0xc8
  IL_0006:  stfld      int32 CSConsoleForTest.Child::cD
  IL_000b:  ldarg.0
  IL_000c:  call       instance void CSConsoleForTest.Parent::.ctor()
  IL_0011:  nop
  IL_0012:  ret
} // end of method Child::.ctor
可以看到Child类的构造函数会自动调用Parent类的构造函数,在调用基类构造函数之前先初始化自身的字段cD。
Parent类的构造函数生成的IL指令如下:
.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
  // 代码大小       16 (0x10)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.s   100
  IL_0003:  stfld      int32 CSConsoleForTest.Parent::pD
  IL_0008:  ldarg.0
  IL_0009:  call       instance void [mscorlib]System.Object::.ctor()
  IL_000e:  nop
  IL_000f:  ret
} // end of method Parent::.ctor
可以看到Parent类又调用了最顶层基类Object的构造函数,同样是在调用基类的构造函数之前初始化自身的字段i。
由此,我们得到一个结论:
当创建对象时,CLR会自动调用类的构造函数,在此构造函数中,先初始化自身的字段,接着调用基类的构造函数,这是一个递归的过程,一直到递归调用到最顶层基类Object的构造函数,然后再返回。
2.静态字段的初始化顺序
静态字段的情况与实例字段略有不同,请看以下示例。
class Parent
{
    public static int pS = 100;
}
class Child : Parent
{
    public static int cS = 200;

上述代码编译之后,您会发现C#编译器自动生成了一个.cctor方法 
 .cctor方法 
.cctor方法完成的工作是初始化自身所在类型的静态字段。以Child类为例,C#编译器生成的IL指令如下: 
.method private hidebysig specialname rtspecialname static 
        void  .cctor() cil managed 

  // 代码大小       11 (0xb) 
  .maxstack  8 
  IL_0000:  ldc.i4     0xc8     //将200压入计算堆栈 
  IL_0005:  stsfld     int32 CSConsoleForTest.Child::cS 
  IL_000a:  ret 
} // end of method Child::.cctor 
可以看到,Child类的.cctor方法不像.ctor方法一样,会自动调用基类的.cctor方法。 
其实,.cctor方法称为“类型构造器”,C#使用静态构造函数的方法实现。上述Child类如果不在定义静态字段的同时初始化,也可以在类的静态构造函数中进行初始化。 
class Child : Parent 

    public static int cS ; //不明确指定初始值 
    static Child()  //类型构造函数 
    { 
        cS = 200; 
    } 

简单地说:C#编译器为静态构造函数生成一个.cctor方法。 
对于静态构造函数,需要记住的就是: 
类的静态构造函数用于对静态字段进行初始化,它不会自动调用基类的静态构造函数。 
在了解了两种字段不同的初始化顺序之后,我们来看看这两种类型字段在内存布局上是否也不一样。 

实例字段的内存布局
请看以下代码:
class Parent
{
    public  int pD=100;
}
class Child:Parent
{
    public  int cD=200;
}
以下代码创建一个Child类的对象。
Child   objChild=new Child();
创建完对象后,内存布局如图
                                   Child对象
                                 ------------
                                 |同步快索引 |
 ------------                ------------
|Child对象引用|------>  |类型表指针 |
 ------------               ------------
                                 |   pD=100  |<-----Parent类实例字段
                                 ------------
                                 |   cD=100  |<-----Child类实例字段
                                 ------------
                                 托管堆
以上很清晰地展示出类的实例字段的存放方式。子类对象“集成”了基类的实例字段。
4.静态字段的内存布局 
请看以下代码: 
class Parent 

    public static int pS = 100; 

class Child : Parent 

    public static int cS = 200 ; 
    //静态方法访问静态字段 
    public static void VisitStaticField() 
    { 
        cS += pS ; 
    } 

Child类中有一个静态方法VisitStaticField(),在此方法中可以同时访问子类和父类的静态字段。 
上述代码对应的内存布局如图
             Parent类型表
             -------------------                     Object类型表
             |     ....        |                           -------------------                  
             -------------------                    |     ....               |                  
             |基类型表指针     |  ---------->    -------------------                  
             -------------------                    |基类型表指针       |-----------               
             |   pS=100        |<- --               -------------------              |         
             -------------------     -              |   方法表            |             _____          
             |     ....        |   -        -              -------------------             ___           
             -------------------     -
                                             -
                                             -
                                             -
                                                  -
             Child类型表                        -
             -------------------               -
             |     ....                |                 -
             -------------------                    -
             |基类型表指针        |                     -
             -------------------                       -
             |   cS=200           |   <-----静态类型字段
             -------------------
             |     ....                |
             -------------------
             |VisitStaticFIeld()  |  <-----类型的静态方法
             -------------------
静态字段的内存布局
可以看到静态字段和静态方法都保存在类型表中,子类类型表通过一个指针与基类型表联系起来。在图4-25中,Child类型表的基类型表指针指向Parent类型表,而Parent类型表的基类型表指针又指向Object类型表。由于Object是最顶层的基类,所以其基类型表指针为空。
需要注意的是,虽然子类和基类类型表通过指针相连,但子类型中的静态方法访问父类型的静态字段并不需要使用此指针,C#编译器将相应的代码编译为ldsfld或stsfld指令,直接定位到了相应的类型表,不需要在程序运行时“临时”通过类型表指针去查找。

posted @ 2009-09-29 17:27 435银狐 阅读(...) 评论(...) 编辑 收藏