深入了解.NET中继承和多态(上)

注意:在写完文章后很久才发现自己文章中【编译】两个字的让人误解,比如方法槽偏移量是在编译时获得的,其实我想表达的是JIT编译,而不是指IL编译。我大概修改了一些关键地方,但是可能有很多遗漏。大家要自己判断了,哈!

封装、继承、多态是面向对象的最重要的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的类型对象,对象中记录了这个类型的静态字段、方法,所有这个类型的实例对象的类型对象指针都指向这个类型对象。见图-3(更详细的描述参见《CLR Via C#》第2版P90)

                                               图-3

以调用一个方法的过程是,通过栈上的引用变量找到GC堆上的对象,通过这个实例对象的类型对象指针找到它自身的类型对对象,然后从类型对象中的方法表中调用对应方法(这里指一般情况,不涉及继承和多态)。而这个类型对象是存放在默认程序域中的。

 

 

3:方法表结构

 

 图-4

这是一张很经典.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


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

3 评论

  1. 回复:zhvsby
    呵呵,你可以继续看后面2篇关于多态的文章。看完了应该就明白了。CoreCpu继承与IntelCpu ,所以会继承IntelCpu 的虚方法表,而Fun是虚方法,所以CoreCpu实际拥有2个Fun方法。一个是继承的,一个是自己new的。

    Cpu c3 = new CoreCpu();
    c3.fun();
    在IL编译时,Cpu这个类型的fun方法是虚方法,所以编译出的fun3的地址是:方法表地址+方法偏移。运行时发现实际对象是CoreCpu,所以会找到CoreCpu对象的方法表,方法偏移指向的是继承的那个fun方法,而不是自己new的。

    详细的解释还是看第2篇。

  2. 不错 可以咨询个问题么。CoreCpu : IntelCpu 里的Fun是如何做的 ,因为InterCpu里的fun 已经override了。对于实例化 Cpu c3 = new CoreCpu();
    c3.fun();
    我始终认为应该是做的Cpu 里的fun 但是结果却是InterlCpu里面的。为何啊?难道是CoreCpu继承了interCpu的缘故么?恳请赐教。

cc_net进行回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注