針對基類引用符指向派生類對象引起的思考

針對《扣響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、沒有派生類進行虛函數重寫或隱藏基類方法時,基類引用符雖指向對象,但被分配的訪問權限卻被僅僅定格在堆中那些基類所擁有的成員上。
 
image
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();
 }
 }
}
輸出:
image
輸出的第1到第3行,不用贅述了。
第四行,Display()方法一直重寫到Mammal類,所以當Animal類型的引用符指向Cat類的對象時,引用符所訪問的Display()方法實際上是Mammal類重寫後的方法。但this依然是代表真正的對象,所以纔會有如此的輸出。
 
當Mammal類中的Display()前加new,也就是隱藏Display()方法時,發生了new關鍵字在虛方法繼承中的阻斷作用,即Animal類中的虛函數Display()在Mammal類中繼承中斷了,只在Vertebrata類中完成了繼承並重寫,所以Animal類引用符也只能訪問到Vertebrata類中重寫的虛函數了。
輸出如下:
image
第4、5、9、10行的輸出明顯發生了變化。這些現象正好可以用上面分析論述進行解釋。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章