封装、继承、多态是面向对象的最重要的3个特点。但是想真的弄明白他们其中的奥秘还是的费一番功夫。记得在学校学习C++的时候,讲到这个地方,自己早已是一头雾水,当时还在想,弄成private做什么,多麻烦啊。到了多态,继承更是昏死了。今天就来深入了解下其中的奥秘吧。
本文主要是从内存结构出发来讲解.NET中的继承和多态,因为内存布局的不同所以和其他语言中的继承多态可能有一定区别。
一 笔试题目
class Program
{
static void Main(string[] args)
{
Cpu c1 = new Cpu();
c1.fun();
Cpu c2 = new IntelCpu();
c2.fun();
Cpu c3 = new CoreCpu();
c3.fun();
IntelCpu c4 = new CoreCpu();
c4.fun();
}
}
class Cpu
{
public Cpu()
{
Console.WriteLine("初始化Cpu");
}
public virtual void fun()
{
Console.WriteLine("Cpu的方法\n");
}
}
class IntelCpu : Cpu
{
public IntelCpu()
{
Console.WriteLine("初始化IntelCpu");
}
public override void fun()
{
Console.WriteLine("IntelCpu的方法\n");
}
}
class CoreCpu : IntelCpu
{
public CoreCpu()
{
Console.WriteLine("初始化CoreCpu");
}
public new void fun()
{
Console.WriteLine("CoreCpu的方法\n");
}
}
上面是我们常见的关于继承和多态的题目。或许很多人都有一套做这种题目的方法,能够让你准确的得到答案,但是我们了解继承和多态不是为了背公式,不是为了做题目,是未来灵活使用。所以有必要弄清楚她内部到底是怎么实现的。或许平时可能用不上,但是我认为还是会有所帮助的。
二 继承和多态的基础
什么是继承和多态?教科书和网络上都有很多感念解释。这里就不进行解释了。如果对这2个概念还不太了解,我想也很难把这篇文章看下去。
在学校学习C++的时候,老师会告诉我们什么是继承,还会告诉我们c++中有公有,私有,保护等几种继承方式。说的最多的就是继承提高了代码重用。而在C#中,是只有公有继承的。也就是说子类会继承父类所有了字段、方法和属性(不包括构造方法)并且在子类中是可以访问的,而且继承是具有传递性的。
我们可以在子类对象中调用父类的方法,以达到代码重用的目的。如果我们对父类对象的方法进行重写,那么我们的子类就有了自己的特殊方法,与父类行为有所不同,于是又引入了多态。而多态在C++中让我记得最深的一句话就是可以通过指向父类对象的指针,调用子类的方法。当时就觉得很玄乎,明明是指向父类,怎么就调用子类了。当时能力有限,也无法深入研究,后来看COM相关的东西,谈到接口,谈到了方法表,我才有了点大概影像,原来是有张表记录了方法的地方,当然移动指针就可以调用不同方法了。但是每当看到Cpu c2 = new IntelCpu(); \这样的代我码我就头痛不已。我根本无法弄明白到底是按Cpu还是IntelCpu去调用方法。
三 .NET内存结构
在深入了解继承和多态之前,有必要了解下.NET的一个内存结构。因为一切对象和方法的调用是离不开线程栈和GC堆的。所以我们需要先了解对象在.NET下是如何表示的。
1:CLR中域结构
图-1
上图是一个.NET程序运行后的情况,在CLR执行托管代码的第一行代码前,会创建三个应用程序域(系统域、共享域、默认程序域)。系统域负责创建和初始化共享域和默认应用程序域。它将系统库mscorlib.dll载入共享域,并且维护进程范围内部使用的隐含或者显式字符串符号。所有不属于任何特定域的代码被加载到系统库SharedDomain.Mscorlib,对于所有应用程序域的用户代码都是必需的。它会被自动加载到共享域中。默认域是应用程序域(AppDomain)的一个实例,一般的应用程序代码在其中运行。然后我们看到下面有进程堆、JIT堆、GC堆和LOH堆。其中JIT堆是存放编译后代码的地方,而GC和LOH是存放对象的地方。那么对象和各自的方法是通过方法表(Method Tables)连接起来的。而这个方法表就是我们所要讨论的核心。
2:对象在堆栈中的结构
图-2
在前面的.NET学习笔记中有介绍对象在堆栈的分配情况和结构。可以参见:NET学习笔记(三) ------系统类型和通用操作。
我们知道对象的变量是保存在线程堆上的,而引用对象是保存在堆上的。每个对象都有额外的两个字段,分别是同步索引块和类型对象指针。
什么是类型对象?我们知道在运行程序时,CLR系统会加载3个域,其中最重要的就是我们的默认程序域。在图1中我们可以看到默认程序域的结构。在加载程序域时,CLR会根据程序集中的元数据来构建各个自定义类型的对象,比如我们定义了一个CPU内,那么这个就构建一个CPU的类型对象,对象中记录了这个类型的静态字段、方法,所有这个类型的实例对象的类型对象指针都指向这个类型对象(更详细的描述参见《CLR Via C#》第2版P90)
所以调用一个方法的过程是,通过栈上的引用变量找到GC堆上的对象,通过这个实例对象的类型对象指针找到它自身的类型对对象,然后从类型对象中的方法表中调用对应方法(这里指一般情况,不涉及继承和多态)。而这个类型对象是存放在默认程序域中的。
3:方法表结构
图-3
这是一张很经典.NET的内存结构图,其中的主体就是我们提到的方法表,方法表是存放在默认程序域中的是通过对象的类型指针(TypeHandle)和GC中的对象联系起来的。实际这里的TypeHandle是指向方法表的,方法表是放在程序域的高频堆中的。类加载器在当前类,父类和接口的元数据中遍历,然后创建方法表。在排列过程中,它替换所有的被覆盖的虚方法和被隐藏的父类方法,创建新的槽,在需要时复制槽。而类型对象的虚接口图,接口数量也会记录在方法表中。从上图还可以看到,在方法表中有块白色的区域,这个叫方法槽表(Method Solt Table),它指向各个方法的描述,前面我们知道实际JIT编译后的指令是存放在JIT堆中的。而在方法槽表下面是静态变量存储区域。所以我们这里说的方法表就是前面说的类型对象,而其中决定类型行为的方法保存在方法槽表的区域中。
4:方法槽表
其面一直谈的对象的内存结果,这里终于谈到我们的重点内容,方法槽表了。从图-3我们可以看到这个方法槽表的结构。最开始是方法槽数、任何类型的开始4个方法总是ToString, Equals, GetHashCode, and Finalize。这些是从System.Object继承的虚方法。然后后面是类型从基类继承的虚方法,接着是自己类型实现的方法,最后是构造方法。方法槽表的主要就够就是:虚方法--实例方法--构造方法,这样的排序。这里要特别主要的是,基类的实例方法和静态方法是不会继承到子类的方法槽表中的,这里和我们之前理解的,子类会继承父类所有的非构造方法是不同的。因为继承是逻辑上的,而这里是物理上的结构。也就是说,一个类型的方法表槽中,只有父类的虚方法和自己定义的方法(暂不管那4个方法和构造方法)。理解了方法槽表的结构,讲有助与理解继承和多态的本质。
5:备注:
如果想对.NET中CLR创建对象的过程和结构有详细了解请参考:
深入探索.NET框架内部了解CLR如何创建运行时对象:http://www.microsoft.com/china/MSDN/library/netFramework/netframework/JITCompiler.mspx?mfr=true
四 SOS扩展调试
前面介绍完了一个对象在CLR中的结构,这将有助于我们弄清继承和多态,但是在讲解继承和多态之前,先的说下VS中的调试。因为我们需要借助一些调试工具,来查看CLR在运行时内存对象的信息。在程序中我们使用sos进行扩展调试,关于详细命令信息MSDN中有介绍,我也是在网上大概看了下,会用点简单的。下面给大家2个参考地址,大概知道怎么使用就行了。
http://www.cnblogs.com/happyhippy/archive/2007/04/11/710930.html
http://www.rainsts.net/article.asp?id=598