同名笔记《你必须知道的.net》(二) 继承的本质与引用类型内存机制

上一篇博文也说了一点关于内存的知识,但是不详尽,这篇博文彻底的理解了从.net层面理解  引用类型的内存分配--->引用类型的堆内的工作,以及继承的本质。

(说的不对,大家指正)

继承
面向对象:实现单继承和接口多继承
对于.net通过访问权限的修饰符控制安全:public protected internal private
抽象方法和虚方法才可以被重写override,而且虚方法不能是private才可以被重写,抽象方法必须是public.
接口的默认是公共抽象的方法,而且被继承了,必须被实现。


1.继承机制的执行

View Code
public abstract class Animal
{
public abstract void ShowType();
public void Eat()
{
Console.WriteLine("Animal always eat.");
}
}
public class Bird : Animal
{
private string type = "Bird";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
private string color;
public string Color
{
get { return color; }
set { color = value; }
}
}
public class Chicken : Bird
{
private string type = "Chicken";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public void ShowColor()
{
Console.WriteLine("Color is {0}", Color);
}
}

public class TestInheritance
{
public static void Main()
{
Bird bird = new Bird();
Chicken chicken = new Chicken();
}
}


 

分析:
1.程序入口依旧是 Main函数,当 Bird bird;时,堆栈中为bird分配4字节的内存存储指针指向堆中的对象实例地址。
2.new Bird()时,如下:
2.1先计算需要在堆中分配的内存,计算内存是从子类开始---基类结束。
2.2调用构造函数,构造Bird类型的Type对象,静态方法,方法表,接口等,分配在Load Heap上,所以方法表式优先于对象分配内存的。
只有用到的类的方法表才会被加载进Load堆。
注:任何类型方法表中,开始的4个方法总是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize。
2.3 初始化Bird的两个附加成员:TypeHandle和SyncBlockIndex。
2.4调用构造器,进行实例字段的初始化,初始化的过程是从基类-父类-子类。(由上一篇图可知,是向高位扩展的方式,所以附件成员在下,字段在上)
字段的排序是:父类的字段在子类的字段前面,如果同名编译器会认为是不同的字段。
2.5 IL代码newobj分配内存的地址将传递给引用变量:bird变量。

注意:方法表是在类第一次加载到AppDomain完成的,而且生命周期是直到AppDomain被卸载。
如果再有新的对象实例被创建,只是将对象的附件成员TypeHandle指向方法列表的Load Heap地址上。

3.当Chicken chicken时,大致是一样的,但是会把父类的方法复制一份,然后与自己的方法列表比较,是否覆盖
4. new Chicken()时,同上。

注意:而且为对象实例分配内存,堆中式由下而上的,向高位扩展,类似于NextObjPtr指针是向上移动的。而在堆栈中,堆栈指针式向下移动的。
在回收内存时,必须GC堆被回收后才会回收Load堆。


结论:对于堆栈,是向低位扩展,所以指针是向下移动的。
对于GC堆,对象的实例是向高位扩展的,所以指针是向上移动,但是对于初始化实例,存储过程是在对象所占内存中是由上到下,即在GC堆内部父类字段在前,子类字段在后。
对于Load堆是属于堆的,所有也是从低位向高位扩展存储分配,对于Load堆内部的方法表父类的方法在前,子类的方法在后。任何类型方法表中,开始的4个方法总是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize。
 
- 继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时可以添加新方法。
- 子类可以调用父类方法和字段,而父类不能调用子类方法和字段。
- 虚方法如何实现覆写操作,使得父类指针可以指向子类对象成员。
- 子类不光继承父类的公有成员,同时继承了父类的私有成员,只是在子类中不被访问。
- new关键字在虚方法继承中的阻断作用。


疑惑1:Bird bird2 = new Chicken(); 调用方法的时候到底调用哪一个类的方法?子类?父类?
答:调用子类方法还是父类方法,取决于创建的对象是子类对象还是父类对象,与引用变量的类型无关。引用类型的不同只是决定了不同对象在方法表中的不同访问权限。
如下代码:

View Code
class Program
{
static void Main(string[] args)
{
P1 p1 = new P2();
p1.S();//因为是P1类型的所以无法访问P2的方法

P2 p2 = new P2();
p2.S1();
p2.S();
}
}
class P1
{
public void S()
{
Console.WriteLine("我是父类方法");
}
}
class P2 : P1
{
public void S1()
{
Console.WriteLine("我是子类方法");
}
}



如果这样声明:
Bird bird2 = new Chicken();
Chicken chicken = new Chicken();
答:根据上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不同的类型指针在虚拟方法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址区域内执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不同的类型访问具有不同的访问权限问题。


- 执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird_type(注意编译器是不会重新命名的,在此是为区分起见),如果type类型设为public,则在此将返回“Bird”值。这也就是为什么在对象创建时必须将字段按顺序排列,而父类要先于子类编译的原因了。

注:也就是我们所说的  引用类型不同决定的是 访问权限问题。

疑惑2:如果在子类内 new 父类的同名方法会怎么样?
答:关于new关键字在虚方法动态调用中的阻断作用,也有了更明确的理论基础。在子类方法中,如果标记new关键字,则意味着隐藏基类实现,其实就是创建了与父类同名的另一个方法,在编译中这两个方法处于动态方法表的不同地址位置,父类方法排在前面,子类方法排在后面。

Type is Chicken,为什么,这里就没有就近原则?
如下代码:
 

View Code
public abstract class Animal
{
public abstract void ShowType();
public void Eat()
{
Console.WriteLine("Animal always eat.");
}
}
public class Bird : Animal
{
public string type = "Bird";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
private string color;
public string Color
{
get { return color; }
set { color = value; }
}
}
public class Chicken : Bird
{
public string type = "Chicken";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public void ShowColor()
{
Console.WriteLine("Color is {0}", Color);
}
}

public class TestInheritance
{
public static void Main()
{
Bird bird2 = new Chicken();
Console.WriteLine(bird2.type);//bird 就近原则

bird2.ShowType();//Type is Chicken,为什么,这里就没有就近原则?

}
}

疑惑3:bird2.ShowType();//Type is Chicken,为什么,这里就没有就近原则?
答:这里创建的是 Chicken对象,所以加载父类Bird方法表和自己的方法表,如果有重写就覆盖,这里覆盖了,所以没有使用就近原则。
上面 bird2.type,因为没有被覆盖,编译器认为是2个不同的变量,所以就近原则。

原文地址:https://www.cnblogs.com/IAmBetter/p/2341325.html