我们知道在定义类字段的时候可以对其初始化,如果子类和父类都有多个字段需要初始化,那么初始化的顺序如何呢?
实例化字段
测试代码如下:
class A
{
public int m = 5;
}
class B : A
{
public int n = 8;
public B(int v)
{
n = v;
}
}
static void Main(string[] args)
{
B a = new B(10);
}
使用ildasm工具查看生成的B类构造函数的IL代码
可以看到,在生成的B类构造函数中,先初始化了自己的字段,然后调用基类构造函数,最后执行构造函数剩余部分代码,而在执行基类B的构造函数时同样先初始化类B本身的字段,然后调用基类object的构造函数,接着执行B类构造函数的剩余部分。
因此我们可以得出结论:
当创建对象时,CLR会自动调用类的构造函数,在此构造函数中初始化自身字段和调用基类的构造函数,这是一个递归的过程,一致递归到.Net最顶层基类Object的构造函数,然后再返回。
静态字段
静态字段的初始化与实例字段略有不同,测试代码如下:
class A
{
public static int m = 5;
}
class B : A
{
public static int n = 8;
}
使用ildasm工具查看
c#编译器自动生成了一个.cctor方法,完成的功能是初始化自身所在类型的静态字段,但它不会调用基类的cctor方法。cctor方法称为'类型构造器",或者叫"静态构造函数",c#编译器会为每一个拥有静态成员的类型生成一个类型构造器方法,例中字段n如果不在定义的同时初始化,也可以在静态构造函数中初始化,代码如下,c#编译器会为静态构造函数生成一个.cctor方法,因此我们可以说:类的静态构造函数用于对静态字段进行初始化,它不会自动调用基类的静态构造函数
class B : A
{
public static int n;
static B()//静态构造函数
{
n = 8;
}
}
实例字段的内存布局
测试代码如下:
class parent
{
public int pD = 100;
}
class Child : parent
{
public int cD = 200;
}
Child objChild = new Child();
创建完对象之后,内存布局如下图所示
静态字段的内存布局
测试代码如下:
class parent
{
public static int pS = 100;
}
class Child : parent
{
public static int cS = 200;
public static void VisitStaticField()
{
cS += pS; //访问静态字段
}
}
上述代码对应的内存布局图如下
可以看到静态方法和静态字段都存在于方法表中,子类方法表有一个指针与父类方法表联系起来,Child类的基类型方法表指针指向Parent方法表,而Parent方法表的基类型方法表指针指向Object方法表,由于Object是最顶层的基类,所以其基类型方法表指针为空。
虽然子类和基类方法表通过指针相联,但子类中的静态方法访问父类的静态字段并不需要使用此指针,这是因为c#编译器会将相应的代码编译为ldsfld或stsfld指令,在这些指令中直接定位到了相应的类型方法表,不需要在程序运行时“临时”通过类型方法表指针去找。
这些相关联的方法表,能够为程序运行时的类型动态转换提供方便。因此CLR通过类型方法表知道某个类型的父类和子类是什么,从而对能否将此类型转换为特定类型作出判断。