再探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指令,直接定位到了相应的类型表,不需要在程序运行时“临时”通过类型表指针去查找。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述