再探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指令,直接定位到了相应的类型表,不需要在程序运行时“临时”通过类型表指针去查找。
一个类中可以有多个字段,我们可以在定义字段的同时对其进行初始化。如果子类和父类都有多个字段需要初始化,那么,其初始化的顺序是怎样的呢?
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指令,直接定位到了相应的类型表,不需要在程序运行时“临时”通过类型表指针去查找。