昨天考试中遇到一道这样的题目,大概是如下形式:
下列关于c#说法错误的是:(选择两项)
A 类可以实例化为对象 B对象可以实例化为类
C类可以调用非静态成员 D对象可以调用静态成员
一眼扫下来,我发现BCD三个选择都是错误的,BC选择错的很明显,而D选项的错误在于静态成员只能由类型对象来调用,而实例对象是不能调用静态成员的。回到寝室,我愈来感觉问题似乎不像我想象的那么简单,我发现从理论上说实例对象也应该是可以调用静态成员的,至少有途径可以办到,为了将这个问题阐述的更加清楚,让我们来分析一下托管堆中创建对象和对象运行时在内存的一些情况。
首先对于托管应用程序来说,内存中最为重要的布局有两个地方,一个是线程堆栈,也就是应用程序调用方法,保存变量,指针的内存区域,另一个便是托管堆,堆中的内存常常被用来创建引用类型的对象,而值类型的对象一般在线程堆栈上创建。托管堆受CLR和垃圾收集器的控制,保证其无用对象被恰时释放和内存的正确分发。应用程序中创建的对象引用的类型作为指针保存在线程堆栈上,而对象的实际类型则在托管堆上保存。设想如下代码:
class A{}
class B:A{}
class C{
private static void Main()
{
A a=new B(): //此处的a引用在堆栈上保存,而new出的实例B在托管堆上保存。
}
}
为了方便和完全的讨论,我们假设此时内存上尚未创建任何对象,此时CLR正欲编译执行如下代码:
1 A a=new A();
2 A b=new B():
3 B c=new B();
当代码运行到1时,CLR首先会在内存托管堆上创建一个type对象,由于任何引用对象都包含除本身的字段和方法外,还有另外的两个数据结构:类型对象指针和同步索引块,类型对象指针用来指向该对象本身的类型,而同步索引块用来同步数据和保证线程安全,故type对象也会初始化这两个数据结构,而它的类型对象指针指向的便是它本身,其后该对象初始化内部所有的静态字段,再提领元数据得到所有的方法列表,保存在对象中,完成这个操作之后,CLR会发现A,B类型的对象需要在接下来的操作中被实例化,于是CLR首先会在托管堆中分配相应大小的内存来保存这两个类型的类型对象,所谓类型对象,就是在未实例化实例对象之前,CLR会先在内存中建立需要实例化的类型的模板,而该模板中保存了实例化该对象所需的静态字段和所有的方法列表。然后,当CLR发现接下来操作的类型都已经建立了相应的类型对象之后,CLR就开始进行实例化的操作,第1行代码需要实例化一个A类型的实例对象,对于new关键字作为对象实例化的操作来说,内存中实际的操作有:
1 计算本类以及所有层次上的基类所定义的所有的实例字段,与创建类型对象指针和同步索引块所需要的所有字段之和。以此确定该对象的大小。
2 从托管堆中分配指定大小的字节数来创建对象,分配的所有字节都初始化为0。
3 初始化类型对象指针和同步索引块
4 递归调用类型构造器,初始化所有字段为可用状态。
CLR执行完这些操作之后,返回该对象的首地址指针作为引用保存在堆栈上。
此处的A类型对象的类型对象指针会被初始化为指向type类型对象。而A类型实例对象的类型对象指针会被初始化为指向A类型对象。这样,无论引用该实例对象的引用是什么类型,都可以通过类型对象指针查找到该类型的实际类型,这一点是CLR在执行类型检查,保证不被引用的虚假类型表象所欺骗的绝对保证。
从以上的讨论中,可以得到如下事实:
实例字段保存在实例对象中,实例字段保存在对象堆中,通过实例对象的引用获得。故可用 实例对象.字段名 来引用实例字段。
静态字段和方法列表保存在类型对象中,在方法未编译之前,方法表的地址都指向编译入口,直到应用程序调用该方法时,通过编译器载入该方法的IL代码,即时编译成本地机器代码,然后替换其入口地址,此后,该方法再次被访问到时,便可以以本地代码运行速度全速运行。而实际的方法体代码保存在线程堆栈中(如果该方法代码已经编译的话)。
下面将要讨论非虚实例方法,虚实例方法,静态方法的调用情况。
当一个非虚实例方法被调用时,CLR在线程堆栈中查找该引用获得其定义的类型(并不一定是该引用指向对象的实际类型),然后直接转到该引用定义的对象的类型对象中去查找方法列表,得到该方法在堆栈中的方法体,再跳转到首地址执行,如果在该类型对象中未查询到该方法,说明调用方法继承自父类,于是CLR会回溯类层次结构,直到查找到该类型方法为止。事实上非虚实例方法的调用是不需要实例对象真实存在的,换句话说,即使对象为NULL,理论上CLR也能正常的调用到定义的非虚实例方法,因为CLR根本不会理会该引用指向的实例对象,而是直接查询到该类的类型对象,而类型对象是一定会创建在内存中的,但C#语法强制规定必须为引用创建实例对象才能调用非静态方法,这一点使得无法为空引用调用方法体。
当一个虚实例方法被调用时,CLR在实例对象中查找其类型对象指针,得到该类型本身的类型对象,然后调用其中定义的方法,此处解释了如果一个父类的引用引用子类的实例,调用的虚方法为何为子类本身重写的方法,原因就在于虽然是父类引用,但在调用虚方法时,CLR会直接到实例对象中去查询该实例本身的类型对象,从而会确定的引用到该实例本身的类型对象上来。
当一个静态方法被调用时,由于只能由类名调用静态方法,所以其类型对象很容易确定,之后得到方法体在堆栈中的偏移,便可以调用该类型对象的静态方法。
整理后得到如图
好了,当以上所有基础知识讨论完毕之后,我们可以来设想一下,应该如何调用,才可以让实例对象得到静态方法的引用。当一个对象实例被创建之后,类型对象指针必然保存了该实例对象所属的类型对象的地址,从而可以直接指向其类型对象,而一个类型的类型对象中保存的是该类型的静态字段与所有的方法列表,包括静态方法和非静态方法。CLR此时便可以通过入口地址替换的方式,从编译入口替换到静态方法体入口地址,从而可以让程序转到该静态方法去执行。
所以,我觉得从技术上讲实例对象是可以调用静态成员的。但c#语法根本不会允许这种情况的发生。经过思考,我觉得可能与面向对象的编程方式与思想有关。静态成员本质上是一个固定存在的个体,不应该属于任何一个实例的个体,更由于静态方法没有隐式的this对象值传入,从而也超出了实例同步索引块的管辖范围,那么很可能引起数据无法同步或者线程安全等各种问题,也许正是基于这些原因,才让语言的设计者们做出了一致的决定——实例对象无法调用静态成员,静态成员只能是类型对象的特定属性。
原文的链接:http://blog.sina.com.cn/s/blog_5efce6a70100ca1d.html
相关文档资料
同步索引块:http://www.cnblogs.com/qianyz/archive/2011/10/26/2224925.html
类指针:
http://topic.csdn.net/u/20090408/11/a9d4f2f7-3dd0-4e45-9a48-1e8b881552aa.html