這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
1、C++ 中的“介面”
C++並沒有明確的“介面”,一般約定繼承某個類,已達到介面的“實現”。
首先我們來看下單繼承的記憶體布局(<font color= Crimson size=4>依賴各廠商的實際實現,這裡僅以微軟實現為例進行說明····感謝宇宙最強IDE····</font>)
其多態主要由虛函數表(vfptr)實現 : 指標或引用調用虛函數時,在運行時由對象的虛函數表+函式宣告順序決定綁定到哪個函數上
class IDuck {public: //嘎嘎地叫 virtual void GaGaSpeaking() = 0; //老爺的官步 virtual void OfficialWalking() = 0; private: unsigned int height;};class DonaldDuck : public IDuck { public: void GaGaSpeaking() { std::cout << "DonaldDuck Speak" << std::endl; } void OfficialWalking() { std::cout << "DonaldDuck Walk" << std::endl; } };int main(int argc, _TCHAR* argv[]){ DonaldDuck * duck = new DonaldDuck(); duck->GaGaSpeaking(); duck->OfficialWalking(); cout << "---------- hooking --------" << endl; typedef void(*DuckFunc)(); int * addr = (int*)duck; DuckFunc f1 = (DuckFunc)(*((int*)(*addr))); f1(); DuckFunc f2 = (DuckFunc)(*((int*)(*addr)+1)); f2(); return 0;}
記憶體布局為:
強制調用成員函數(甚至可以是私人)
多繼承
多繼承下其實也是類似,按繼承順序依次排列,還是看程式碼範例
class IDuck {public: //嘎嘎地叫 virtual void GaGaSpeaking() = 0; //老爺的官步 virtual void OfficialWalking() = 0; private: unsigned int height;};class IActor {public: //搞笑 virtual void MakeFun() = 0;private: std::string Name;};class DonaldDuck : public IDuck, public IActor { public: void GaGaSpeaking() { std::cout << "DonaldDuck Speak" << std::endl; } void OfficialWalking() { std::cout << "DonaldDuck Walk" << std::endl; } void MakeFun() { std::cout << "Wa HAHAHA ~~~" << endl; }};int main(int argc, _TCHAR* argv[]){ DonaldDuck * duck = new DonaldDuck(); duck->GaGaSpeaking(); duck->OfficialWalking(); duck->MakeFun(); cout << "---------- hooking --------" << endl; typedef void(*DuckFunc)(); int * addr = (int*)duck; DuckFunc f1 = (DuckFunc)(*((int*)(*addr))); f1(); DuckFunc f2 = (DuckFunc)(*((int*)(*addr)+1)); f2(); typedef void(*ActorFunc)(); int * addr2 = (int*)(*(int*)((IActor*)duck)); ActorFunc f3 = (ActorFunc)(*(int*)(addr2)); f3(); return 0;}
記憶體布局為:
菱形繼承(略)
更多請參考
2、Go中的介面
Golang將interface作為一種類型,並且不依賴繼承,而是以一種類似於duck-typing的實現。所謂duck-typing,是一種動態類型風格:當一個obj走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼它就可以被稱為鴨子。
既然Go並沒有像C++那樣要求主動告訴編譯器需要繼承哪個父類,那麼是如何?動態類型的呢?(基於Go1.6,1.7及之後版本由於nameOff不方便gdb列印)
首先,interface由兩部分組成{tab, data},其中tab儲存了介面的中繼資料,這個很重要。
type iface struct { tab *itab data unsafe.Pointer}
itab中比較重要的有interfacetype及fun[],其中interfacetype儲存了該介面需要實現哪些方法,fun[]則儲存動態類型是如何?這些方法的
type itab struct { inter *interfacetype _type *_type link *itab bad int32 unused int32 fun [1]uintptr // variable sized}type interfacetype struct { typ _type mhdr []imethod}type imethod struct { name *string pkgpath *string _type *_type}
附:一篇經典論文中的圖解
research!rsc: Go Data Structures: Interfaces
e.g. 唐老鴨的go版本
package mainimport ( "fmt")type Duck interface { GaGaSpeaking()() OfficialWalking()()}type Actor interface { MakeFun()()}type DonaldDuck struct { height uint name string}func (dd *DonaldDuck) GaGaSpeaking()() { fmt.Println("DonaldDuck gaga")}func (dd *DonaldDuck) OfficialWalking()() { fmt.Println("DonaldDuck walk")}func (dd *DonaldDuck) MakeFun()() { fmt.Println("DonaldDuck make fun")}func main() { dd := &DonaldDuck{10, "tang lao ya" } var duck Duck = dd var actor Actor = dd duck.GaGaSpeaking() actor.MakeFun() dd.OfficialWalking()}
我們用gdb調試一下
首先,看下結構類型與兩個介面的記憶體關係
可見,duck與actor的data指標都指向dd
然後是Duck介面的方法集:
以及其動態類型的具體實現:
可以看到,都指向了tanglaoya的具體實現
再看看Actor的方法集:
及其動態類型的具體實現
3、總結
C++在代碼編寫時就明確了是否實現某個介面,並將介面資訊附加在自己的記憶體中,但is-A的模式越來越限制模組間的解耦;Golang其寬鬆的介面充分降低了耦合的發生,但可能在代碼書寫無意中卻實現了某個介面.. 此外,其實現可能會比較繞,容易發生其他錯誤(比如經典的interface與nil的比較等等)