Crest的OO核心實現
至此,我們已經有了一個比較好用的宏封裝的代碼風格,一個基礎的類對象結構的模擬代碼,但是始終沒有接觸到OO的核心實現。這次,我們終于要進入實質的內容了---實現對象的繼承和接口的實現。
單對象的內存布局
我們在《Crest的語法---宏的魔術匯演》實現了一個簡單對象,含有成員變量,成員函數,如下圖所示。其中buffer指向字符緩沖區,虛函數Format表現為一個函數指針。
這種結構把數據和成員函數一起放到了實例對象中,但是這個結構一旦遇到大規模的對象使用場景的時候,就會浪費很多空間,因為對于相同對象而言,其成員函數都是完全一樣的,沒有必要每一個對象都保留一份成員函數副本。所以我們可以把這樣的結構修改一下:
這樣就簡單了,一個CString類在內存中可以有很多個實例,但是都共享同一個CStringType對象,CStringType對象負責存放虛函數和純虛函數的指針。這個所謂的CStringType在C++里邊叫做VTable,在delphi中叫做VMT,其實所有的OO語言都存在這個東西。
比較仔細的同學可能就有些疑問了,看前一篇文章中的代碼,Format是虛函數,OnFormat是純虛函數,DoFormat則是實函數,按照道理,實函數應該不要出現在CStringType中的。是的,因為我們用C語言的struct來模擬class,本來struct就不支持public / protected / private等可見性定義,模擬出來的都只能是public的,這是限制之一。如果我們還要嚴格區分虛實函數的話,也會因為C語言的限制,導致虛實函數調用語法的不一致,反而引來很多麻煩,更何況java也不區分虛實函數,它所有的成員函數都是虛函數,都是可以重載的,所以就算借鑒java的特性吧。
對象的繼承結構
單個對象我們已經完成了內存布局的設計,接下來我們要實現類的繼承。
要實現繼承結構面臨的第一個需求就是基類和派生類的相互轉換。在編程實踐中我們已經知道,基類和派生類之間,派生類可以當作基類一樣來使用,就像 CObject obj = new CString() 這樣,反過來也可以這樣CString str = (CString) obj實現強制轉換。大多數單繼承結構的類都用簡單的方法實現都是保證CString的前半部分和基類CObject的內存結構完全相同。
如上圖所示,如果一個指針指向CString結構,因為內存結構相同,也就等同于指向了一個CObject結構。
也存在另外一種基類和派生類的轉換方式,就是COM的QueryInterface的方式,所有的類型轉換都通過調用QueryInterface來獲得,這樣的基類、派生類甚至接口,甚至都可以用不同的對象來實現,不過代價很大,暫時不用。
針對一個CString類,我們現在需要CString和CStringType配合使用才能完成一個整個類的功能,基類CObject同樣也需要CObject和CObjectType配合。因此基類和派生類同構的原則同樣要適用于CStringType和CObjectType之間。這樣,從圖中我們可以看到,一旦CString把自己的toString實現的函數指針填入CStringType的toString位置,就可以實現所謂的"重載(override)"。
這里要注意的是,自此,我們之前談的this指針就隱含劃分為兩個部分,一個是CString,一個是CStringType。
但是我們如果要調用基類的函數呢?C++的語法是CObject::Format(xxx),java語法是super.Format(xxx),C#的語法和java類似。這里的關鍵是要不要在CStringType中設置一個指向CObjectType的指針。從易用性看,java/C#的方法更簡單,C++的方法則有信息重復的嫌疑,萬一修改基類名稱了,C++需要修改不止一個地方,而java/C#則只需修改一個地方。但是我們的Crest實現的時候,如果用C++方式實現,則就可以不需要再CStringType中放置一個指針,否則就需要。這個在單片機編程時候可能更在意一些。決定放置一個指針,結構就變成了下圖的樣子:
接口實現
一個基本的單繼承結構已經設計完畢,接下來要準備實現接口(interface)。和基類相比,接口就簡單得多,因為接口不涉及到數據,所以所有的工作就集中在我們如何來調整CStringType上。
在具體設計接口之前,我們要談一下理論。如果把一個類CString的所有成員函數看作是一個集合a的話,CString實現的任意接口b中包含的全部函數集合c都是a的子集。所以,接口的實現其實不過就是把宿主對象的成員函數復制出來,按照某個規定的順序重新排列一下而已。接口和基類之間的另外一個區別就是接口不需要宿主對象與之同構,而且不同的類型實現的相同接口之間的唯一相同的地方就是函數的排列順序。
具體實現起來,我們就用一個額外的表來存放它,我們稱之為InterfaceChain。
這樣的結構已經可以支持IFormat fmt = (IFormat)new CString()語句了,但是CString str=(CString)fmt呢?出錯了!所以我們要給每一個InterfaceChain元素添加一個owner,讓他們指向CStringType,這樣我們可以用CString str = (CString)fmt.owner來講interface實例指針轉換回對象指針。
以上設計的是我們的核心結構,而且隨著未來特性的增加,會越來越復雜。這樣的結構當然不能展示給用戶使用,相反隱藏得越深越好。這一切還是要拜托神奇的macro來完成了。具體如何來封裝和引用這些結構,下一次再講。

公眾號:老翅寒暑
浙公網安備 33010602011771號