针对基类引用符指向派生类对象引起的思考
针对《扣响C#之门》书中第九章中引出的虚方法继承使用,产生了很多疑问(感谢该书能激发读者的深思,刚开始学其他书时想都不会去想这些问题),关于这部分内容的确值得深入,先对基类引用符指向派生类对象引起的思考进行分析:
1、当派生类继承基类时,实际上是将基类所有成员全部继承下来(除了sealed声明的密封函数或密封类),当创建派生类对象时,不论派生类是否重写或隐藏了基类的成员,原基类的这些成员依然会被生成于托管堆的GC Heap堆中(而且这些成员应该在内存中写在派生类成员前面的位置),只是派生类对象本身不再访问那些基类中私有的成员或者被派生类重写或隐藏的成员。
2、在出现 声明基类引用符,并将派生类对象的指针赋给它时,如Animal ani1=new Cat(); 首先因为创建的是派生类的对象,当用 ani1.GetType()函数时,返回的是Cat类型,原因是该函数是针对对象的!对象的实际类型当然是派生类型了。
但,引用符被声明时,针对引用符访问的区域却未必相同(这应该也是多态性的体现吧),不同类型声明的引用符在虚拟方法表中(即托管堆中的Loader Heap堆)只能访问特定的区域(这个区域是由对象在GC Heap堆中的Type Handle类型句柄指向的),在GC Heap堆中堆成员变量的访问也是只能访问特定的区域。具体阐述如下:
1)所有的变量和成员的访问(不论值类型还是引用类型)都是在一个区域内由高地址向低地址进行访问(这样说不知道合理否?)。
a、值类型时,栈的分配是由高地址向低地址扩展,所以变量作用域才会有此规律:程序中前面的变量作用域最大,因为先进栈,放在了高地址处。
b、引用类型时,引用符是存放在栈中的,符合FILO原则,但其指向的对象在堆中存放是由低地址向高地址扩展。因此,当声明一个派生类的对象时,基类的成员变量和成员函数都放在派生类的成员前面,也就是放在低地址。
正常的派生类引用符指向派生类对象,派生类引用符将获得派生类及其所有基类的所在区域访问权限,因为派生类成员存放的地址高于基类的存放地址。不论派生类是否重写或隐藏了基类的函数,根据“执行就近原则”,派生类引用符只会去访问离其开始访问最近的那个方法。
而基类引用符指向基类对象,基类引用符是不可能拥有其派生类的区域访问权限的,一则根本就没有分配该部分内存,二则即使有,地址也会比基类成员高,基类引用符访问不到。
2)对于声明基类引用符,并将派生类对象的指针赋给它时,应针对以下情况进行讨论
a、没有派生类进行虚函数重写或隐藏基类方法时,基类引用符虽指向对象,但被分配的访问权限却被仅仅定格在堆中那些基类所拥有的成员上。
b、如果派生类进行了虚函数重写,基类引用符访问权限将扩展到最后一个重写其虚函数的派生类所对应的重写方法中。即此时的基类引用符访问方法表中的地址已经提高到派生类重写其虚函数的地址处,根据“执行就近原则”,基类引用符当然访问被重写后的同名方法了。
c、如果派生类对基类的某个成员进行了隐藏,且此成员为成员变量或者此成员不是虚函数,则访问权限和1)中所述一样。
d、如果派生类对基类的虚函数没有重写,且对其进行了隐藏,则基类引用符访问权限和2)中所述一样,我们也可称此时对虚方法的隐藏(new),是new关键字在虚方法继承中的阻断作用。
因此,既然是基类声明的,那么该引用符只能访问基类的对应成员,因为不论是成员变量还是成员方法都在不同的堆中,排在派生类的成员前面,就近原则就是也就是说虽然创建的是派生类对象,但由于声明的是基类的引用符,该引用符只能访问基类内的成员变量和成员函数,对于那些被重写的虚方法(注意是那些没有用new隐藏的方法),访问基类成员方法权限将扩展成访问派生类中重写后的方法。
试验程序如下:
using System; using System.Collections.Generic; namespace Test { public class Animal { public string type = "type is Animal"; protected string name; public virtual string Name { get { return "调用自Animal的Name " + this.name; } set { this.name = value; } } public virtual void DisplayName() { Console.WriteLine("Animal类传下的Display方法 this.Name值为:" + this.Name); } } public class Vertebrata:Animal { public new string type = "type is Vertebrata"; public override string Name//对属性Name进行了覆写 { get { return "调用自Vertebrata的Name "+this.name; } set { this.name = value; } } public override void DisplayName()//对方法DisplayName()进行了覆写 { Console.WriteLine("Vertebrata类传下的Display方法 this.Name值为:" + this.Name); } } public class Mammal:Vertebrata { public new string type = "type is Mammal"; public override string Name { get { return "调用自Mammal的Name " + this.name; } set { this.name = value; } } public override void DisplayName() { Console.WriteLine("Mammal类传下的Display方法 this.Name值为:" + this.Name); } } public class Cat:Mammal { public new string type = "type is Cat"; public override string Name { get { return "调用自Cat类的Name " + this.name; } set { this.name = value; } } } public class Program { static void Main() { Cat catAnimal = new Cat(); Animal ani1 = catAnimal; //以上两句,一个是派生类的引用符,一个是基类的引用符,都指向了同一个派生类对象 //ani1.type="ani1";//此句和下面的语句分别赋值,都成功,且用下面语句输出时,也能呈现相应的值,说明不同类型的引用符的确是各自访问各自类型中的成员 //catAnimal.type = "catAnimal"; Console.WriteLine(ani1.type);//发现此句和下句输出值不一样,说明不同类型的引用符,即使指向同一个对象,且两个类型中有同名的成员,无重写关系,两引用符分别访问对应类型内的成员 Console.WriteLine(catAnimal.type); Console.WriteLine(ani1.GetType());//此句返回对象的类型,不是引用符的类型 //按照排列组合,分别对基类和各个派生类进行了初始化,同时用不同类型的引用符指向它们。对象生成的次序都是从辈分最低的派生类开始。 Animal ani2 = new Mammal(); Animal ani3 = new Vertebrata(); Animal ani4 = new Animal(); Vertebrata vert1 = new Cat(); Vertebrata vert2 = new Mammal(); Vertebrata vert3 = new Vertebrata(); Mammal mam1 = new Cat(); Mammal mam2 = new Mammal(); Cat cat1 = new Cat(); //分别调用可以重写的虚函数,看看效果 ani1.DisplayName(); ani2.DisplayName(); ani3.DisplayName(); ani4.DisplayName(); Console.WriteLine(); vert1.DisplayName(); vert2.DisplayName(); vert3.DisplayName(); Console.WriteLine(); mam1.DisplayName(); mam2.DisplayName(); Console.WriteLine(); cat1.DisplayName(); } } }
输出:
输出的第1到第3行,不用赘述了。
第四行,Display()方法一直重写到Mammal类,所以当Animal类型的引用符指向Cat类的对象时,引用符所访问的Display()方法实际上是Mammal类重写后的方法。但this依然是代表真正的对象,所以才会有如此的输出。
当Mammal类中的Display()前加new,也就是隐藏Display()方法时,发生了new关键字在虚方法继承中的阻断作用,即Animal类中的虚函数Display()在Mammal类中继承中断了,只在Vertebrata类中完成了继承并重写,所以Animal类引用符也只能访问到Vertebrata类中重写的虚函数了。
输出如下:
第4、5、9、10行的输出明显发生了变化。这些现象正好可以用上面分析论述进行解释。