程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 第十章 類、對象與實現

第十章 類、對象與實現

編輯:C++入門知識

第十章 類、對象與實現


第十章 類、對象與實現

萬物都是容器,容器的符號是U;對象就是單個容器的別名。一切皆對象,具有某些相同屬性特征的對象歸納成類。對象(Object)是類(Class)的一個實例(Instance);類是對象的模板。如果將對象比作房子,那麼類就是房子的藍圖。我們以自然語言去描述世界,而計算機是用各種數據結構去描述世界;數據可以用x個二進制位的位容器BUx來表示。對象具有狀態、方位、時間等屬性,每個屬性是用數據值或說位容器BUx來描述的;最後構造了描述對象屬性的數據表。對象還有操作,用於改變對象的屬性,操作就是對象的行為。操作也可以說是方法或函數,方法就是一段指令;一條指令就是一個數據字W;所以,方法就是一個指令字數組。由一系列方法構成的指令數據表就描述了對象的行為。所以,一個對象就是由屬性數據表和方法數據表來描述的。或者換句話說對象實現了數據和操作的結合,使數據和操作封裝於對象的統一體中。在APO中,數據表不外是一個行(8字)對齊的位容器,是一段32字節或8字對齊的存儲空間;和C語言的結構struct類同。數據表中的元素最小是一個字(4字節、2字符),並最終是行對齊。

一、變量與表

1、變量

變量:用來標識(identify)一塊內存區域,這塊區域的內容是可變的。
變量名:是一個標識符(identifier),用來指代一塊內存區域,即變量。

CPU是用內存地址來標識一塊內存區域的,機器碼中,是不會出現變量名的;出現的是邏輯相對地址。在匯編層次上,操作的都是地址,不存在任何名稱;變量名是給我們程序員操作內存來使用的。簡單說,變量就是地址;在對程序編譯連接時由編譯系統給每一個變量名分配相應的內存邏輯地址,形成符號表;該地址分配後不可改變。當你調用此變量時,編譯器就會根據此符號表找到對應的邏輯地址,然後翻譯成匯編指令。例如從R2寄存器為基址的表中讀一個變量所在行的內容到行寄存器H5,翻譯成匯編:MOV H5, R2(變量邏輯相對地址); 結果是:( R2 + 變量邏輯相對地址變量 ) -> H5。在一個數據表中的邏輯相對地址是指變量地址與表初始地址0的相對偏移,編譯器把邏輯相對地址、變量名、變量類型等一起放在符號表中。所以,變量名並不占內存空間;在APO中,變量邏輯相對地址(簡稱變量地址)是一個字1W。


程序是由一些類文件組成,一個具體類就產生2張表(屬性表、方法表)。在編譯時產生的類文件的2張表都是從地址0開始的,在連接時將各個類文件進行符號替換,這時會修改相應的表邏輯相對地址,最後產生一個2張合表都從地址0開始的程序文件。在運行時加載器會把程序的2張合表加載到某個不定的內存區域中(每次加載到的物理內存地址不一定相等)。當然,許多類屬性表或方法表合作一起,程序運行時還有很多對象在參與;應該有一個方向指示總表:對象頭列表。對象屬性表、方法表或全局變量或靜態變量(包括一些復雜類型的常量),它們所需要的空間大小可以明確計算出來,並且不會再改變,因此它們可以直接存放在程序文件的特定的節裡(而且包含初始化的值),程序運行時也是直接將這個節加載到內存中,不必在程序運行期間用額外的代碼來產生這些變量和初始值。其實在運行期間再看“變量”這個概念就不再具備編譯期間那麼多的屬性了(諸如名稱,公有、私有等類型,作用域,生存期等等),對應的只是一塊內存(只有首址和大小),所以在運行期間動態申請的空間,是需要額外的代碼維護,以確保不同變量不會混用內存。

一個程序有大量的屬性表、代碼表、屬性元素、數組、字符串等等變量;CPU不認識變量名字,只認識地址數字;所以,編譯器的任務之一就是把各種變量名稱翻譯成相應的地址了。變量所指元素的數據類型在許多語言中都有很多說法,如整形,單精度,雙精度,字符串等等。真實情形是CPU不認識它們,只是編譯器、方法(代碼段)認識它們。APO是匯編語言,沒那麼多說法。簡單地,APO變量的數據類型只有位容器;但需考慮對齊。表不外是x行容器BUxH,表中的元素或許是一個字,或是32位對齊的位圖,或是字(字符)數組,或是一個表,或是另一個表的引用(動態數組,動態字符串,表)。一個數據塊是64KH(行)= 1MZ(字符)= 512KW(字)= 2MB(字節)。

2、內存區域

APO在內存分配時會涉及到以下區域:

◆寄存器:用於自動變量,如方法裡的局部變量。

◆棧:存放臨時的基本類型數據、方法的局部變量。

對象的引用(包括動態對象)、動態分配內存的變量引用,局部靜態變量等都應該保存在對象的屬性表中;但動態對象、動態變量的身體是存放在堆中。棧是會自動清除或覆蓋數據的,適合自動變量。APO對堆內存的操作和對棧內存的操作速度是一樣的,比寄存器操作稍微慢點;即使是動態內存分配和釋放(有專用的硬件指令)不過是幾個ns。

當在一段代碼塊定義一些變量時,編譯器就在棧中為這些變量分配內存空間,當這些變量退出該作用域後,棧會自動釋放掉(重新設置棧SP寄存器值)為這些變量所分配的內存空間,該內存空間可以立即被另作他用。棧中的數據大小和生命周期是可以確定的。

在APO中,一個進程內只有一個用戶棧,所有的對象(包括線程對象)都是共用一個棧。棧的大小,是全局變量;默認是2個頁,256行;如果對象數目多或很多線程,就可設大些;最大是32KH。為何JAVA等是每個線程需要一個2MB棧,大量占用內存?我認為這是設計問題。在APO中,對象消息處理代碼塊內只是一些少量的自動變量;普通對象完成消息處理後就會自動清除,相對來說不占空間;而線程對象可能會因等待消息,從而暫時讓出CPU,但並不讓出占據棧內的那部分空間;就可能會同時有多個線程占據棧空間。解決辦法就是增大棧空間吧;APO中一個進程就算有6萬個對象在活動,半個數據塊的棧空間就足夠了。編譯器將自動變量翻譯成對棧指針(0地址)的相對偏移地址。

◆堆:存放用動態產生的數據

堆內存用來存放由動態創建的對象和數組等動態變量。在堆中分配的內存,由APO的內存管理員來管理或由用戶代碼釋放。動態聲明(用DYV標識符)一個變量,DYVBU64KH A;那麼系統就會自動為變量A分配一個數據塊,64KH行的內存空間,並返回首指針,大小給代碼保存好。這時,在編譯器的符號表中沒有變量A的相對地址;而是需要另外的代碼通過保存在對象屬性表中的首指針去操作變量。聲明BU1W[512K] A;字數組 或者BU1Z [1M]A;百萬個字符串數組;實際上也是分配一個數據塊。定義向量或說動態數組BUxW[]也是可以的,可能會使用到連續的數據塊或數據塊鏈表。但內存總是有限的,還不如用數據流,只是開辟一個數據塊窗吧;數據流可以無限。動態內存的生存期由程序員決定,使用非常靈活,但如果分配了空間,就有責任回收它,否則運行的程序會出現內存洩漏,頻繁地分配和釋放不同大小的空間將會產生內存碎塊。當然,在APO中系統內存管理員也會定期清理垃圾並回收內存。對象的析構方法也會據對象引用計數為0時釋放相應的對象內存。

   在代碼塊中以表聲明、或在代碼塊和類中以向量聲明、DYV聲明的變量都放到堆區,都當作是動態對象;系統會返回分配的塊號、行數、行開始地址。並將後3項保存到動態對象頭列表相應項中。由於動態變量的地址,編譯器無法預先定義;所以是不會把動態變量翻譯成地址的。動態變量在符號表中的1W數據,高16位依然是動態變量的行數大小;但低16位不再是相對地址,而是唯一指示動態變量的動態對象號。這一字數據會傳入CPU的寄存器A;很簡單,編譯器只是多匯編出一條到A寄存器的立即數傳送指令吧。對於數組動態變量,還需另外傳送數組的16位寬度的成員項數和成員位寬度到CPU的寄存器B。B和A(行數)都放在動態對象頭,而A的動態對象號卻形成間接引用指針放在對象引用變量中。接著,用戶需以寄存器A的參數調用內存管理員中的方法獲得內存地址等數據並保存,之後是初始化方法,否則系統默認初始化為0。我真不明白JAVA為何要在棧建動態變量符號表?不多余?

在堆中產生了一個動態數組或動態對象後,還可以在相應對象屬性表中定義一個特殊的變量,讓對象屬性表中這個變量的取值最終找到數組或對象在堆內存中的首地址,對象屬性表中的這個變量就成了數組或對象的引用變量。 引用變量就相當於是為數組或對象起的一個名稱,以後就可以在程序中使用對象屬性中的引用變量來訪問堆中的數組或對象。引用變量是普通的變量,定義時在對象屬性表中分配,引用變量在程序運行到其作用域之外後被釋放(還在的,但如果釋放內存,會變為空指針);而動態數組和動態對象本身在堆中分配。動態數組和動態對象在沒有引用變量指向它的時候變為垃圾;不能再被使用,但仍然占據內存空間不放;在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)。所以,用戶對象退出時調用析構方法;而析構方法要包含釋放對象內存代碼。

  ◆常量池:存放常量(一般放在方法表區)

 

3、表

表的定義:BUxH 表名字{ n個元素… },表中的每行一個元素的聲明以;結束。BUxH意思是表占有x行(16字/行)的存儲空間;不寫也行,由編譯器據元素占用情況定。類的定義:class 類名字{ 類屬性表{},類方法表{}}。程序的所有具體類,編譯器都實例化一個與類同名的對象出來;並為它們分配好邏輯地址空間,分配對象號,建立對象頭列表。具體類對象屬性表也可以一開始就初始化(在類文件中設置),或由系統默認清0,或另外寫一個初始化(構造)方法。全局表或全局變量或靜態變量(包括一些復雜類型的常量)和對象頭表、類表及一些公共方法,建立一個程序根類來實現。在編譯器符號表中,對象名對應的地址值:高16位是所屬的類號;低16位為對象號。在堆中分配的動態變量(包括對象、向量、大數組等)全部視為動態對象,編譯器對於代碼塊中的對象、動態變量(用DYV聲明)都分配動態對象號;可由對象序號構成對象頭表。抽象類(包括接口),雖然沒有實例化的對象,但有類關系(描述繼承關系)、抽象屬性,還有抽象方法。抽象類(包括接口)和具體類構成樹結構;所有的類名、類關系、抽象屬性名字,還有抽象方法名字等等最終組成類表。類表、對象頭表、對象屬性表、具體類方法表為四大要素表。

1)、類表

金字塔形的樹狀類表,有三個層次:

1))、純抽象類:只有抽象屬性和抽象方法;接口是只有抽象方法的純抽象類。只有抽象屬性的類為概念類。

2))、普通抽象類:可有抽象屬性和普通屬性,可有抽象方法和普通方法。

3))、具體類:只有普通屬性和普通方法。

類描述的是該類成員(成員也可能是類)的特征,與類的所屬關系;還有行為或說成員所具有的能力。偉大的、光榮的、無產階級革命家雷鋒同志。電腦如何理解呢?偉大、光榮是2個概念類;無產階級革命家、同志是有抽象屬性和抽象方法的純抽象類;有一個多繼承於它們的隱藏具體類;雷鋒是具體類中的一員;毛澤東、周恩來等也是其中一員。大海中的海生動物類中的龜類中的小龜類中的mm小龜正在 游玩。這是金字塔中,從上往下的類描述,我們的語言通常是隱藏了中間的抽象類層次;只是說明路徑的2端。如大海中的mm小龜 正在游玩。大海是根類;小龜是具體類,mm是具體類中的一員;游玩是小龜類的能力或其具有的抽象方法,而“正在”是“游玩”方法的時間描述,也就是方法的屬性或說是變量。“游玩”是一個抽象方法,也可以搞成一個接口。不止是小龜的“游玩”,狗、豬、牛也可以繼承這個接口。不知道大家暈了沒有?我是差不多要暈倒了。

金字塔的頂端之後是什麼?如“物類”,物之頂是“虛無”嗎?我不知道!但一些根類的之後,我大約了解一些。比如“水果”這抽象類,並不能說它是繼承於果樹類;只是果樹類中;花、果、等等抽象屬性中的一種。果樹類繼承植物類,植物類繼承生物類,生物類繼承於物類。“水”之後是什麼?是分子式類中的H2O屬性嗎?顯然,抽象類的屬性可以是類;這不是繼承方式,是另外一種包含方式,稱為包含類。具體類的屬性可以是另一個對象的引用,但不能是類。

每一個類都有與類名字對應的類名序號。一個類有派生類和包含類,它們的類名序號構成字數組。這是類的子類描述變量,子類數量可以是變動的,我們應該視電腦為另一種智能生命;而不是死物。隨著電腦的不斷學習、不斷進步;子類數量就可能增加。


每一個類都可能繼承多個父類,我們把繼承的父類序號,包括到根路徑上的繼承鏈上的所有父類序號一起構成類繼承關系字數組變量;數組裡的所有父類序號是唯一的、一個序號只能出現一個。一個對象的初始化方法或析構方法,必須按照對象所屬類的繼承關系數組中的具體類來進行。子類變量和類繼承關系變量是每個類必有的屬性;要記住 繼承就意味著全盤接收父類的方法和屬性。對於行為來說是使用抽象類還是接口,就要看實際情形了。子類變量提供了一條由上到下的路徑,這會破壞對象的封裝性;所以,接口和抽象類要慎重設計。類關系數據庫文件是人類知識庫中的一個文件,類名序號需要用32位來表達數十億的公共類。一個程序裡的類應該不多,默認最大是256個;用戶最多可設置到2K個。類都可以實例化,只不過是具體類實例化成對象吧。接口也是一種抽象類,理論上,其實例化是子類變量和類繼承關系變量;
接口調用是:對象名.接口方法名字();

但編譯器如發現該對象所屬的類有相同的方法名,就直接編譯成直接調用對象所屬類的方法了。所以,接口實例化的好處只是知道接口之間,類與接口之間的繼承關系吧。

類表:以類號順序排列的類項構成的表;每類項占4行存儲空間;最多8KH。

類項:前2行為類繼承關系字數組,可標識32個父類以上的類號。第3行是子類變量,可標識16個下級子類號。對於類繼承關系變量或子類變量;如果第一個字符是0xA5A5,表示多於32個繼承關系或16個子類,需要動態變量來擴充;隨後的3個字符是動態變量指針(所占行數、塊號、行開始地址)。對於抽象類,第4行也屬子類變量。對於具體類,第四行:16位一個參數,方法表指針(方法表所在的塊號、方法表的行開始地址),方法數,方法表所占的行數;本類的對象數,屬性表的屬性項數,屬性表所占的行數。

16位的類號:高3位為類的類型,從111到000依次是:根類、純抽象類、概念類、接口、普通抽象類、包含類(也是小根類)、具體類、類庫(只有方法的具體類)。最低2位為00;指向該類項的第0行地址。中間十一位是類的序號,最多標識到2K個類。

對象頭表:以16位對象號順序排列的1W對象屬性表入口指針(塊號,行開始地址)構成的表;最多64K個對象占用8KH。


在自然界中,存在就是合理;我們不應改變自然規則,使自己處於狹窄的時空。

2)、編譯器符號表簡單介紹

表變量BBL對應的地址值:

BUxHBBL{};//高16位行數x,低16位行開始相對地址;塊號在對象頭表。

元素或屬性或說是變量的聲明:BUxW 變量名字; // 注釋…。
BUxW聲明了變量的數據內容包含x個字,或說是2x個字符串。BUx 聲明了包含x個位,BUxK 聲明了包含x K個位,BUxM 聲明了包含x M(百萬)個位。在APO中,位容器的聲明通常以行(256位)為單位的;因為APO包含有256位為單位的位處理器。

對象屬性表的存儲布局:
這要舉例才好說明,

BU19H表1{ // 19行的表變量。(高16位為行數x,低16位為行開始地址)

DYV G; // G是動態變量的引用變量。對象屬性表可能有n個引用。

// 行開始地址是第0行;行的字地址0;引用變量最小占一行,一行8個引用。

BU32 A; //A變量是32位的位容器或說是2個字符或一個字,BU32 = BU2Z

BU99W B; //B變量是99字的存儲空間,或是198個字符的字符串數組。

// 行開始地址是第1行,行字地址為0,長度99,占行數13行,最後行空5W。

BU32 C; //C是32位的位容器;A、C變量可插入第13行的空洞。

BU1K D; //D是1K位的位圖,也可能是一個字數組,取決於你的代碼。

// D變量行開始地址是第14行;行的字地址為0,長度32W,占行數4行。

BU4W E; //E變量行開始地址是第18行;行的字地址為0,長度4

BU2W F; //F變量行開始地址是第18行;行的字地址為4,長度2

引用變量內容定義:高16位是動態變量所占行數,低16位為對象號。動態對象序號在對象號區,0xC000- 0xFFFF。8K

表內的變量可看作全是1W的數組,對於靜態表來說;要實現不同寬度數組就要定義表內表了。而動態數組就靠程序員了;當然,對於動態數組;編譯器還是會多編譯一條傳送數組的16位寬度的成員項數和成員位寬度到CPU的寄存器B的指令的。

編譯器翻譯原則:

1)、先編排4W的變量在一起,其次是2W的變量;因它們可能是數值,也易於對齊。動態變量的所有引用變量(1W一個)放在一起成字數組,放於表頭,最少占1行(8個)。

2)、多於4W的變量地址都是以行地址開始。

3)、最後是在有空洞的行插入1W/2W/3W的變量地址。

表內變量對應的地址值定義:

BU32 BL;

Bit31 T; // 為0,則Bit30–16的15位是字長度;所以變量的內容最多32KW或4KH。如需要大內容變量,那就要定義對動態變量的引用了;在運行時再分配空間。

// T標志為1是變量的內容長度小於1行; Bit29–24的6位為長度及行的字開始地址。Bit30為1為引用變量,Bit29–27:111數組、110向量;100為對象引用等。Bit26–24的3位為行的字開始地址。變量類型屬性字節Bit23–16用於特殊對象。

BU16 HDV; //變量所在的16位的行開始地址。


編譯器翻譯後的變量地址:不到一行的(8位,8位,16位)一組,保留字節為0,超過一行的(16位,16位)一組。

E = (10 100000,0,18);F= (10 010 100,0,18);

G = (11XXX 000,0,0);B = (99,1);D = (32,14);

A = (10 001100,0,13);C = (10 001110,0,13);

一個表必須完全被包在一個數據塊裡,一個數據塊是64KH。我們可以在表內或代碼塊內聲明一個動態變量為一個數據塊,甚至連續數據塊變量;而在表內引用該變量。但最長的字符串只有1M個字符,占一個數據塊。一個數據塊可以裝多個表;或許有空洞。我認為內存合理分配,也應該需要程序員參與。字符串,數組,代碼段在表內只是一個內容不超過32KW的變量。變量、對象的名字等可以用漢語雙拼簡寫或英文簡寫或直接中文也行,APO編譯器支持U編碼的。

方法(method)表聲明:

BUNH 方法表名字{//變量地址(方法表的16位行數,方法表的16位起始行地址)

BUxW 方法1; // 方法1變量地址(0,15位方法的指令長度x,方法的起始行地址)

….

}

方法表中的方法是一個接著一個的以行為單位緊密存放的。調用方式是:

類名(或對象名字).方法名字; 編譯器會由類名或對象名字、方法名字從符號表中得到類號、對象號和方法指令長度、方法的起始行地址,將這4個值和指令長度、行數一起編譯成指令傳到寄存器A、B。接著據A、B寄存器,設置數據塊號;並將方法的起始行地址作為方法入口指針,調用相應的方法。所以,調用任何一個方法必須指明對象名或類名。當然,調用系統中的類則無須指明類名;因為系統中的所有類中的方法都在第一個數據塊的本地內存中;只需要方法名的起始行地址作為方法入口指針,調用相應的方法。對於本類也可以用this來指代。

二、面向對象編程

我們可以這樣說:萬物皆對象,在程序裡一切皆變量(對象、方法、屬性、表)。

面向過程的思想:由過程、步驟、函數組成,以過程為核心;面向過程是先有算法,後有數據結構。

面向對象的思想:以對象為中心,先開發類,得到對象,通過對象之間相互通信實現功能。面向對象是先有數據結構,然後再有算法。

我們知道同一個類的對象可以有很多個,對應的對象屬性表也有很多;但只有一個方法表。具有相同或相似性質的對象的抽象就是類。因此,對象的抽象是類,類的具體化就是對象,也可以說類的實例是對象。一個程序是由很多不同或相同種類的對象組成,以進程的形式在內存中運行時還可能動態增減一些新的對象。進程的執行體是線程對象,也就是說對象或方法的運行必須是通過線程對象來進行。在APO中可以有最少一個線程對象(主線程),最大32K個線程對象。其它活動對象最多也只是32K個;所以在APO中,一個進程最多有64K個打開的對象。但這並不包含打開的文件對象,一個進程打開的文件對象數最大可達64K個;是另外的文件對象管理員管理的。其實,在一個進程裡;通常有4個管理員:文件對象管理員、線程對象管理員、普通對象管理員、內存管理員。管理員是通用的,系統內含的,每一個程序都要繼承它們。我會以編寫新型的APO操作系統為例,來說明面向對象編程的方式、過程。

1、面向對象的特征:

(1) 對象唯一性


在一個進程中,每個對象都有自身唯一的標識(16位的所屬類號和16位的對象號),通過這種標識,可找到相應的對象(方法表、屬性表入口,大小,引用計數)。在對象的整個生命期中,它的標識都不改變。

(2)分類性

任何類的劃分都是主觀的,但必須與具體的應用有關。

(3)繼承性

繼承性是子類自動共享父類數據結構和方法的機制,這是類之間的一種關系。在定義和實現一個類的時候,可以在一個已經存在的類的基礎之上來進行,把這個已經存在的類所定義的內容作為自己的內容,並加入若干新的內容。

在類層次中,子類只繼承一個父類的數據結構和方法,則稱為單重繼承。
在類層次中,子類繼承了多個父類的數據結構和方法,則稱為多重繼承。


在軟件開發中,類的繼承性使所建立的軟件具有開放性、可擴充性,這是信息組織與分類的行之有效的方法;它簡化了對象、類的創建工作量,使程序結構更清晰;增加了代碼的可重性。采用繼承性,提供了類的規范的等級結構。通過類的繼承關系,使公共的特性能夠共享,提高了軟件的重用性。繼承是單方向的,即派生類可以繼承和訪問基類中的成員,但基類無法訪問派生類的成員;這是由編譯器、及公私有等變量限定屬性實現的。被繼承的類稱為父類或超類,繼承了父類或超類的所有數據和操作的類稱為子類或派生類。

1)、繼承的特點:

第一、子類擁有父類的屬性和方法;

第二、子類可以有自己新的屬性和方法;

第三、子類可以重寫父類的方法;

第四、可以聲明自己為父類,創建子類。

2)、多重繼承:

在APO中,可以說表、類繼承關系數組變量就很自然地實現了多重繼承。多重繼承是普遍的、自然的。但多重繼承需解決二義性問題或說是菱形繼承問題。


何時出現二義性?APO如何解決?


1))、當一個派生類從多個基類派生,而這些基類又有同名成員,在對該同名成員進行訪問時,可能會出現二義性。


例子:類C3繼承了2個類C1,C2中的同名方法f(),一個是c1的,另一個是c2的,如果不顯式聲明調用哪一個,編譯器就不知道你要調用哪一個。這時就必須使用作用域分辨符:寫為C3.C1.f();或C3.C2.f();就可以了。但C++非得通過C3的對象才可以,因為f()本質上是C1或者是C2的成員函數,而類成員函數都是要通過對象C3來調用的,如果你想通過C1.f()來直接調用,那麼f()必須是static。為何這樣?如果C1是C3的派生類,那麼在C3對象中調用C1.f()就變成父類可以調用子類的方法了。所以,C3.C1.f();或this.C1.f();編譯器就會查找C3對象是否有對C1的引用,有則調用,否則報錯。APO的調用都是:類名.方法名;而不管類名是父類的、基類的、本類的、庫類的、系統類的、動態類的等等;本類的派生類則報錯。所以,APO調用是可以直接寫為:C1.f()的,編譯器把C1對象號和類號編譯傳過來後,匯編代碼會在C3對象所屬的類表項中的類繼承關系變量中查找是否有C1的類號,如有則從類表項中找到對應的方法表基址;再加上方法表的方法相對地址最終指向方法f()。靜態類由編譯器直接給出方法的相對入口地址;如果C1、C2是動態對象,那只能在C3的對象所屬的類表項中的類繼承關系變量中分別查到C1、C2的類號,沒有則報錯;從而在類表項中找到對應的方法表基址;再加上方法表的方法相對地址最終指向方法f()。所以,APO在這點上是沒有二意的。


2))、當一個派生類從多個基類派生,而這些基類又有一個公共的基類,在對該基類中說明的成員進行訪問時,可能會出現二義性。這就是著名的菱形繼承問題。

當在多條繼承路徑上有一個公共的基類時,在這些路徑中的某幾條匯合處,這個公共的基類就會產生多個副本;從而出現二義性。比如一個類繼承了100個父類,而父類又繼承100個祖父類。。。形成許許多多的路徑,最終都到達10個根類。就算只是2級,都有10000條路徑通向10個根類,也會多產生了9990個對象副本;並造成二意。

在C++中,為解決菱形繼承還引入了一個更扭曲的虛繼承。Python支持多種繼承, 並且因為所有的對象都繼承於共同的基類Object,導致任何多重繼承其實都是菱形繼承,當你真的開始自己用多重繼承時,其實繼承結構已經是一張網了,對此, Guido VanRossum的答案是用深度優先搜索,靠基類排列的順序來決定, 顯的更加神奇。

C++的多重繼承很麻煩,而且又容易出錯。而無數的JAVA用戶者則是把C++的多重繼承當作笑話和C++混亂的根源,自豪的宣布JAVA沒有這個問題。J那就是沒有沒有C++自由的多重繼承, 只支持對實現的單繼承, 並引入了接口, 只允許接口的多重繼承。並且, 在繼承接口和繼承基類形式上特別加了一些區別,所以實際中, 很多人並不把這叫做多重繼承,並宣稱JAVA沒有多重繼承。“組合優於繼承”這一設計原則,體會了JAVA社區提倡的面向接口編程。“繼承本身就是一種強耦合”,就是一種子類對父類的依賴耦合,在一些書籍中提到, 甚至繼承本身都是不提倡的, 也就是說, 提倡的是基於對象的設計(OB), 而不是面向對象的設計(OO)。JAVA最多算半個多重繼承,只是行為多繼承。大家都知道, 加一個繼承只要一個單詞,而加一個對象的組合調用,往往需要增加N個函數接口,以及N個調用(雖然有種東西叫做委托)。

Mix-in類是具有以下特征的抽象類:
不能單獨生成實例
不能繼承普通類

通過這種在接口和普通多重繼承之間的折衷,Mix-in提供了對實現的多重繼承,同時對其進行了限制, 使得繼承結構不會成菱形, 而是和單一繼承一樣的樹型. 在Ruby中, Mix-in是通過模塊(module)的概念來實現的。有趣的事情是,在編程這件事情上, 很多時候, 折衷的方法卻往往是優秀的方法。但不符合自然的東東,終是遺憾。

APO是很自然的支持多重繼承。在對象所屬的類表項中的類繼承關系變量中有類關系數組,包括父類的,所有的關系類都是唯一的;不會出現多副本的情形。對象初始化;是先調用父類對象初始化方法,再對關系數組中的具體類的對象作初始化;其實,對象亂序初始化也沒問題的。

扭曲式多重繼承、循環式多重繼承會產生父子不清的現象。比如,一只黑狗有一個兒子白狗,而黑狗的母親狗又跟兒子白狗生了一只公黃狗。公黃狗是孫子狗?還是兒子狗?還是算父親狗?我都轉暈了。盡管自然界,這種現象極少;但還是有的。這種扭曲多重繼承的特點就是:相互引用就可相互調用;也就是基類可調用派生類的方法。APO的多重繼承也同樣是自然的支持這種扭曲式多重繼承。

(4)多態性(多形性)

多態性是指相同的操作或方法、過程可作用於多種類型的對象上並獲得不同的結果。不同的對象,收到同一消息可以產生不同的結果,這種現象稱為多態性。多態性允許每個對象以適合自身的方式去響應共同的消息。多態性增強了軟件的靈活性和重用性。

(5)、抽象類

邊學、邊抄、邊寫下這段章節後的感想:何為抽象?那是億眾矚目的地方,那是靈魂深處的悠游,那是聯想的發源地,那是萬川歸一的大海,那是世界的最頂峰,也是宇宙的邊緣。從那看進去:一眼星河湧現、神光飛舞、浮想纏綿。

抽象,顧名思義,就是抽掉了具體形象的東西。把具體概念的諸多個性排出,集中描述其共性,就會產生一個抽象性的概念。抽象概念的外延大,內涵小,具體概念的外延小,內涵大。要抽象,就必須進行比較,沒有比較就無法找到在本質上共同的部分;所以抽象的過程也是一個裁剪的、分類的過程。在抽象時,同與不同,決定於從什麼角度上來抽象;抽象的角度取決於分析問題的目的。抽象通過分析與綜合的途徑,運用概念在人腦中再現對象的質和本質的方法,分為質的抽象和本質的抽象。分析形成質的抽象,綜合形成本質的抽象(也叫具體的抽象)。萬物都是容器,類名就是從無數的容器中分離出具有一些相同屬性、相同方法的容器集的名稱;所以分類的過程就是抽象過程。類名就是一種抽象名,我們把類的個體容器稱為對象;所以,也說萬物都是對象。但從另一角度看,“萬物”、“對象”就是一個更為抽象的概念;它們泛指一切。我們根據一些特性,也即是屬性;抽象出“蘋果”類,“蘋果”也有大小、青紅之分等等;所以“蘋果”類下還可再分成具體類,之後才到具體的某個蘋果。類似的還有“香蕉”、“梨子”等等,我們又從這些“蘋果”、“香蕉”、“梨子”等抽象概念中,抽象出它們的共性,得到“水果”類。而“水果”類可以說它們的成員也是類,所以說“水果”是類中類,是抽象之後的再抽象;是屬於抽象類。這還沒完,“水果”源自果樹,是果樹類中的一個屬性。而果樹又是植物類中的一個種類;植物和動物構成生物類;生物類和非生物類才到達最頂端的根——物類。你可以說“茶杯”是物,是非生物;但不能說是“水果”。從這“金字塔”看,最頂端只有一個“物”類,往下只是2個,再往下呢?植物類、動物類就多了,看你的分類角度吧。對於動物來說,簡單的就是死、活動物類;復雜的你知道的。抽象類的盡頭才是具體類,之後是它們的個體;一個個對象。所以,金字塔有4層,第四層:各種對象;第三層:具體類;第二層:抽象類(有多個層次);第一層:最頂層,只有一個抽象的根類。從金字塔層次看來,似乎只有單繼承,其實不然的;如“水果”就不一定就是源自果樹,它也可能來自植物籐類。就拿衣服類來說,衣服,春、夏、秋、冬衣服,長衫,短袖,棉衣等等都是抽象類;自然界太多的抽象類了。可以說,抽象類、具體類、個體對象充滿了我們的世界。我們的語言就是對它們及相互關系的描述;計算機語言也應該如此!

在面向對象領域,抽象類主要用來進行類型隱藏。我們也可以構造出一個固定的一組行為的抽象描述,但是這組行為卻能夠有任意個可能的具體實現方式。這個抽象描述就是抽象類,而這一組任意個可能的具體實現則表現為所有可能的派生類。

抽象類往往用來表征我們在對問題領域進行分析、設計中得出的抽象概念,是對一系列看上去不同,但是本質上相同的具體概念的抽象。比如:如果我們進行一個圖形編輯軟件的開發,就會發現問題領域存在著圓、三角形這樣一些具體概念,它們是不同的,但是它們又都屬於形狀這樣一個概念,形狀這個概念在問題領域是不存在的,它就是一個抽象概念。正是因為抽象的概念在問題領域沒有對應的具體概念,所以用以表征抽象概念的抽象類是不能夠實例化對象的。

1))、語法:


抽象類中除了抽象屬性,還可以有抽象方法,也可以有普通方法,抽象方法只可以被聲明,不能被實例化(就是不能有方法體),必須由繼承該抽象類的普通類來實例化。如棉褲,茶 杯,服 裝,放 大鏡等等;自然界抽象類的多繼承比比皆是;APO也是很自然的支持抽象類的多繼承;使用Abstract 聲明抽象類。

抽象類也有方法的抽象。例如動物是一個抽象類,他的移動方法還沒有確定,因為有的動物是四條腿移動,有兩條腿移動,有的飛,有的爬,但是他們都能移動,這樣你可以把動物定義為抽象類,將動物的移動方法定義為抽象方法,強制繼承它的子類去實現。而且你在動物這個類中移動雖然沒有實現,但是可以調用他。

2))、接口

接口(interface)其實就是一個特殊的抽象類;是抽象類的變體。差別在於接口中的方法必須都是抽象方法,不可以有普通方法而已。既然是一種抽象類,接口就只能繼承其它接口,抽象類和普通類也可以繼承接口和抽象類。在APO中,抽象類可以包含接口!接口可以只是抽象類的行為部分。

接口是對具有的動作能力預定義,抽象類是對具有的動作的抽象。接口是對動作的抽象,抽象類是對根源與動作的抽象。抽象類表示的是,這個對象是什麼?做什麼;接口表示的是,這個對象能做什麼。比如,男人,女人,這兩個類(如果是類的話……),他們的抽象類是人。說明,他們都是人。人可以吃東西,狗也可以吃東西,你可以把“吃東西”定義成一個接口,然後讓這些類去實現它。接口是一種規范和標准,用於約束類的行為,接口可以將方法的特征和實現分割開來。抽象類中可以加入非抽象的方法,而接口是不能的。抽象類是對象的抽象,然而接口是一種行為規范。抽象類對於提供模式、藍圖和後代類遵循的原則有用,如果遵循了藍圖的語義,後代類的行為可能按抽象類提供者和使用者所期望的那樣。抽象類將事物的共性的東西提取出來,抽象成一個高層的類。子類由其繼承時,也擁有了這個超類的屬性和方法;---也就實現了代碼的復用了。子類中也可加上自己所特有的屬性和方法;----也就實現了多態。接口只能定義抽象規則;抽象類既可以定義規則,還可能提供已實現的成員。接口是一組行為規范;抽象類是一個不完全的類,著重族的概念。接口只包含方法、屬性、索引器、事件的簽名,但不能定義字段和包含實現的方法;抽象類可以定義字段、屬性、包含有實現的方法等。

接口(Interface)是用來定義行為的!
APO中的抽象類(Abstract Class)是用來定義與實現行為的!
具體類()是用來執行行為的!

Interface接口表述“has a”關系,用於描述功能是用來說明它的是實現類是“做什麼?”的;至於“怎麼作?”,interface並不進行約束。而abstract class表述“is a”關系,它在描述功能的同時也通過具體實現部分功能的方式,來約束“怎麼作?”。APO中的抽象類也可以有並不進行約束的方法,不就是放在接口上吧。

小結:APO的抽象類、接口首先是一個類;接口只是抽象類的行為部分。但接口包含的內容少,顯得更抽象。

3))、實現簡介

從上面看出,編譯器的符號表很重要;它是電腦與人腦溝通的橋梁;它描述了類名、變量名等與機器數值的一一對應。所以,編寫智能程序應該把符號表包含進去。我們人類用字符語言去描述世界,機器卻是用數值去描述。從某個角度看,可以說抽象類的個體就是一個個類。而抽象類名又指向它們的派生類集;最後,構成一棵類樹結構。類表是一個多樹結構,表中的每一個節點都包含下一個分叉的節點數組——子類描述變量,也包含所有的上級節點數組——類繼承關系變量。對於具體類還有方法表入口、屬性表描述等。

(4)消息和方法

對象之間進行通信的結構叫做消息。在對象的操作中,當一個消息發送給某個對象時,消息包含接收對象去執行某種操作的信息。發送一條消息至少要包括說明接受消息的進程號、對象名、發送給該對象的消息名(即對象名、方法名)。一般還要對參數加以說明,參數可以是認識該消息的對象所知道的變量名,或者是所有對象都知道的全局變量名。

2、方法

類中操作的實現過程叫做方法,一個方法有方法名、參數、方法體。方法只有一份,供所有的同類的對象使用!而屬性是每個對象一份,因為每個對象的都不一樣。


方法重載:就是在同一個類中,方法的名字相同,但參數個數、參數的類型或返回值類型不同!實際上,還是調用不同的方法。


方法重寫:它是指子類和父類的關系,子類重寫了父類的方法,但方法名、參數類型、參數個數必須相同!覆蓋==重寫叫法不同罷了。實際上,父類的方法還在,只是本類的方法與父類的同名吧。


至於加載的意思,其實就是讓編譯器執行某段程序,可以是類可以是包可以是任何編譯器能夠編譯的代碼。

3、公有、私有

面向對象程序設計的重點是類的設計,而不是對象的設計。類可以將數據和函數封裝在一起,其中函數表示了類的行為(或稱服務)。類提供關鍵字public、protected和private 用於聲明哪些數據和函數是公有的、受保護的或者是私有的。這些類型聲明的具體的實現是由編譯器來判斷與進行的。

類是屬性與方法的集合。而這些屬性與方法可以被聲明為私有的(private),公共的(public)或是受保護(protected)的,他們描述了對類成員的訪問控制。

1)、公共的(public):公有是所有包括本類及外部類都可以調用。
2)、私有的(private):私有是只有本類可以調用。這是為了安全和健壯性;保證了不會被別人在外部修改。
3)、受保護的(protected):本類、派生類可以訪問,外部類不行。
4)、默認控制訪問符(friendly):


為了實現數據的封裝,提高數據的安全性,我們一般會把類的屬性聲明為私有的,而把類的方法聲明為公共的。這樣,對象能夠直接調用類中定義的所有方法,當對象想要修改或得到自己的屬性的時候就必須要調用以定義好的專用的方法才能夠實現。提倡的是:“對象調方法,方法改屬性”。


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved