目錄node
part1講述了基於對象,part2則是在基於對象的基礎上,創建類與類之間的聯繫,即面向對象編程以及面向對象設計。ios
主要講述如下三點:git
另外,我把補充內容中的對象模型放入到Part2,我以爲放入這裏更加合適。github
markdown文件和一些代碼能夠從GitHub中下載,連接:https://github.com/FangYang970206/Cpp-Notes, 推薦使用typora打開。編程
轉發請註明github和博客園地址,謝謝~設計模式
Composition(複合)就是has a, 上面的事例就是隊列(queue)類中有一個雙端隊列(deque)類,隊列中的成員函數經過雙端隊列的操做函數完成,這是類與類之間的第一個關係。(黑色菱形表明內部擁有)bash
deque中可能擁有不少方法,但queue中只經過deque提供了很是少的方法,這是一個設計模式Adapter,將一個擁有許多功能的類改造一下,獲得另外一個類。markdown
能夠看到有兩個複合關係,最後queue的內存是40.框架
因爲Container類是擁有Component類,因此在構造方面,先調用Component類的默認構造函數,而後再調用Container的構造函數,由內而外的構造,裏面作好了,再作外面。析構則相反,先對Container進行析構,而後再對Component進行析構,過程是由外而內,將外面的去掉,才能看到裏面去掉裏面,符合常識。函數
若是一個類(string)中擁有一個指針(StringRep*),該指針指向的是另外一個類(StringRep),這種關係是Delegation(委託),更好的說法就是Composition by reference(學術界不說by pointer),兩種類的生命週期不同,與複合兩種類會同時初始化不一樣,委託當須要用的時候再進行初始化。上圖中的實例是一種很是有名的設計,叫handle/body,指針指向的類負責具體實現,能夠看到有一個//pimpl,意思是pointer to implement,而擁有那個指針的類只提供外界接口,就是基於委託這種方式,Handle(string)是提供給外界的接口,body(StringRep)就是實現部分,爲何這麼有名,這是由於String類設計好了就不需修改了,只須要修改實現的那一個類,具備很好的彈性,另外,還有能夠進行共享機制(減少內存),下圖的a,b,c共享一個StringRep,這種方式叫作reference counting,當須要修改其中一個時,須要把內容copy出來一份進行修改,另外兩個依然指向原StringRep。(白色菱形表明指針指向)
繼承的語法就是在類名後加上:public(還能夠是protected,private)你想要繼承的類,若是想繼承多個類,用逗號隔開就能夠了。何時用繼承,肯定一個關鍵點,子類is a 父類(例如,狗is a動物)。上述的List_nodes是繼承了List_node_base全部的數據,另外還有本身的數據。
繼承的類(derived object)的一部分是基類(base part),對於要被繼承的基類,它的析構函數必須是virtual,否則會出現問題,這個問題將在後面說。繼承的構造函數會首先調用基類的構造函數,而後調用本身的構造函數(由內而外)。析構則相反,先析構本身,而後再調用基類的析構函數。
子類繼承了父類的兩樣東西,一種是父類的數據,一種是父類函數的調用權。對於一個類而言,它的子類均可以訪問因此的public方法,而子類要不要從新定義父類的函數呢?這時候就須要虛函數了,當public裏面的函數不是虛函數時,則但願子類不從新定義該函數。當函數是虛函數時(在返回類型前加入關鍵字virtual),則但願子類從新定義它,而且父類已經有了默認定義。當函數是純虛函數時(在結束符;前面加上=0),則但願子類必定要從新定義它,父類沒有默認定義(但能夠有默認定義)。該事例是定義了一個基類shape,而後矩形Rectangle和橢圓Ellipse對shape進行繼承,基類的objectID是無需繼承的,能夠提早定好,在父類調用便可,而error函數,父類有默認的錯誤信息,若是子類有更精細的錯誤信息,父類容許子類能夠從新定義的,打印出子類調用時的錯誤,而draw函數則必須從新定義,父類沒有定義(draw shape沒有意義),子類不一樣,所畫出的形狀天然不一樣。
對於在powerpoint打開ppt文件而言,有如下幾步,先點打開命令的菜單欄,而後出現文件界面,選擇咱們要打開的文件名,而後程序會檢查文件名是否符合規範,符合規範則在磁盤上搜索文件,搜索到了打開文件便可。而遇到注意的是,因此打開文件的過程都是這樣,只有最後打開文件可能會不一樣(可能會打開不一樣格式的文件),因而有團隊就將除文件打開函數之外的函數進行打包,子類直接繼承,只要子類從新定義父類打開文件的函數便可。以下圖所示:
團隊開發了CDocument類,定義Serialize函數須要從新定義,在OnFileOpen函數中的省略號即爲打包好的過程。用CDocument類的人只需從新定義Serialize函數便可,則在main函數中,先建立一個CMyDoc實例myDoc,調用myDoc.OnFileOpen函數,子類沒有定義這個函數,實則調用的是父類的函數,即CDocument::OnFileOpen(&myDoc), 進入父類函數中,運行打包好的過程,當運行到Serialize函數時,發現子類從新定義了它,則調用子類從新定義的Serialize函數,最後再返回到CDocument::OnFileOpen,繼續下面的過程。不再用寫通常的步驟了,完美!這是一種很是有名的設計模式Template method(不是說C++ template),提供了一種應用框架,它將重複同樣的操做寫好,不肯定的步驟留給實際應用設計者從新實現。十年前最有名的產品MFC就是這樣一種應用框架。
深層次的理解,誰調用函數,this就是誰,當調用Serialize函數是,編譯器是經過this->Serialize()調用,因而就調用到了this從新定義的Serialize函數。
上圖就是CDocument和CMyDoc的實例,用cout來模擬步驟,呼應上面兩張圖片。
當同時存在繼承和複合,類是如何進行構造和析構呢?這一節要討論的問題:
狀況2就很明顯了,構造依然是自內而外,析構是由外而內。
對於狀況1,這是侯捷老師留的做業,本身寫代碼判斷,我寫了一個:
#include <iostream> using namespace std; namespace fy1{ class Base; class Derived; class Component; class Base{ public: Base() {cout << "Base Ctor" << endl;} virtual ~Base() {cout << "Base Dtor" << endl;} }; class Component{ public: Component() {cout << "Component Ctor" << endl;}; ~Component() {cout << "Component Dtor" << endl;}; }; class Derived : Base{ public: Derived() {cout << "Derived Ctor" << endl;} ~Derived() {cout << "Derived Dtor" << endl;} protected: Component c; }; void fy1_test(){ Derived d; } } int main() { cout << "Ctor and Dtor test:" << endl; fy1::fy1_test(); return 0; }
運行結果爲:
Ctor and Dtor test: Base Ctor Component Ctor Derived Ctor Derived Dtor Component Dtor Base Dtor
能夠看到先初始化父類(Base),而後再初始化Component類,再初始化本身,析構與構造相反。
下圖也給出告終論。
至此,面向對象的概念說完了,下面進入實例環節。
上述代碼解決的是下圖所示的問題,對同一份文件使用四個不一樣窗口去查看,或者右下角所示的,同一個數據,三種不一樣的viewer查看。數據只有一份,表現多種形式,數據變化,表現形式也會發生變化,要表現這樣的特性,這就對錶現文件的class和存儲數據的class之間關係要有要求,上圖就是下圖的一種解法,23種設計模式之一。Subject類是存儲數據的類,不過類中有delegation,使用了一個vector類用來存放Observer類的指針,這樣Observer類以及它的全部子類均可以導入這個vector中,Observer類至關於表現形式類的父類,能夠有多種表現形式,這些都是子類。update則是子類須要從新定義的方法,不一樣表現形式能夠有不一樣的更新方法。對於Subject類來講,當咱們想建立新的窗口(新的observer類)去查看它的時候,須要對將新的Observer類進行註冊,函數attach就是實現這樣的功能,能夠將新的observer子類的指針加到vector中,註銷的函數沒有寫出來,另外,當數據發生變化時,使用set_val函數,須要使用一個函數去更新全部的observer子類,這就是notify函數乾的事,遍歷vector每個observer指針,調用指針指向的update方法,對錶現形式進行更新。Delegation + Inheritance真的感受好強大呀。
下圖是一個更詳細的Observer解法構建
第二個實例,構建一個文件系統。能夠把Primitive類看成文件類,而Composite類看成目錄類,與平常使用的文件系統同樣,目錄裏面能夠包含目錄,也能夠包含文件,因此目錄裏面存放的不止是目錄自己還能夠是文件,可是須要放入到同一個容器中,想法是使用指針,但文件和目錄是不太同樣的,因此解決方案是將文件和目錄共同繼承Component類,而後Composite類中的容器存放的是Component的指針,這樣就能夠既存放文件又能夠存放目錄了,這是一種經典的解決方案,也是23種設計模式之一,Composite。另外,Component類中還有一個虛函數add,這是給目錄進行繼承的,由於目錄能夠新建目錄和文件,這裏不能設置爲純虛函數,由於文件不能繼承這個函數,文件是不能在進行添加的。
這又是23種設計模式之一的Prototype,Prototype要解決的是要設計一個類,這個類是爲將來的子類所設計的,他能夠訪問和操做將來的子類,這就頗有意思了,都不知道將來的子類是啥,要去訪問這個子類,這是怎麼去作呢?原來在子類內部會申明一個關於子類的靜態變量,就是上圖中的LSAT : LandSatImage,另外這個子類會定義一個私有的構造方法(前面有一個負號,能夠經過定義靜態變量調用私有的構造方法),構造方法裏面會調用父類的addPrototype函數,將靜態變量的指針傳到父類,父類就把傳入的指針(經過父類的指針形式)加入到本身的容器當中,這樣父類就知道子類的類型,就能夠操做子類了,上述操做是這樣的,父類有一個findAndClone函數,根據輸入參數i選擇父類容器中的子類進行clone,返回子類的指針,而clone父類定義的是一個純虛函數,子類必須從新定義,上圖中子類從新定義的是返回新建一個子類,返回它的指針,不過這個新建是調用的另一個構造函數(有一個#號,表明protect),使用private裏面的構造函數是不行的,它是爲父類打通橋樑的,爲了與private裏面的構造函數區分開,形參有一個int類型,這個int類型不會進行使用。
下面的圖片是相關代碼,解釋上面的文字已經說的很清楚了。
父類Image
兩個子類代碼:
主函數:
Prototype例子來自於《Design Patterns Explained Simply》這本書,經典致敬!
面向對象的例子講完了,下面介紹更加深層次的內容,理解面向對象更底層的東西。
這張圖所蘊含的信息量很大,如今一步步來看,首先最右邊有三個類,A,B,C,A是爺爺,B是父親,C是孫子,向上繼承的關係,首先咱們從內存的角度來看類的佈局,對於有虛函數(無論有多少個虛函數)的類來講,它在內存中不只有本身定義的數據,還會多出一個指針,這個指針學名叫作虛指針(vptr),虛指針會指向一個虛表(vtbl),虛表裏面定義的是各個虛函數所在的地址。A類內存中第一個就是虛指針,指向虛表,虛標裏面有兩個指針指向A的兩個虛函數,下面兩個是A類的數據,B類的前三個是繼承了A類的數據以及虛指針,不一樣的是B類從新定義了vfunc1函數,這將更新虛表,會將原來指向A::vfunc1函數的指針改成指向B::vfunc1函數的指針,如圖就是將黃色的0x401ED0變爲了淺藍的0x401F80,C類繼承B類的數據和虛指針,另外還有本身的數據,同時又重載了vfunc1,因此對應虛表中的vfunc1指針也要發生變化。在調用的時候,根據類中虛指針定義的虛表所指向的函數進行調用虛函數,這就是繼承的根本原理,虛函數則是面向對象最強大的武器。非虛函數就是普通的函數,不過前面加上了類做用域。
靜態綁定與動態綁定:當new一個C類時,獲得一個指針p(上圖所示),當經過p調用vfunc1的時候,其實是最下面中間語句進行調用的(其中n爲編譯器給虛函數在虛表中的索引),這是函數的調用方式與c很不同,在c的時代,當編譯器看到函數調用,編譯器會直接調用call XXX(XXX表明地址),地址是靜態的,不會發生變化,這種方式叫作靜態綁定,而C++經過類指針找到虛指針,根據虛指針找到虛表,從虛表中取出虛函數地址進行調用,這是一種動態的調用,不一樣類的指針所調用的虛函數是不同的,這種方式叫作動態綁定,也叫虛機制。
下圖就是一個實例,展現了這種動態綁定的強大威力。
我要設計一個powerpoint,要畫出各類不一樣的形狀,咱們能夠用一個List,承載A類的指針(放指針是由於容器只能放內存大小一致的東西,不一樣的形狀內存不一致),這樣它的全部子類均可以放入List中了,由於都是A類,放入List後,循環遍歷直接調用各自形狀的draw函數就能夠完成要求,這裏走的依然是動態綁定,這是很好的,爲了更加突出動態綁定的好處,這裏要寫出c語言中是如何作的,c語言只有靜態綁定,上述過程須要條件判斷當前的指針是指向哪個類,各類形狀要使用多重判斷,另外,當須要添加新的形狀類時,又要修改條件判斷代碼,這是很很差的,不符合直觀理解,應該像C++的虛函數同樣,指針指向什麼形狀,就應該調用那種類型的draw。因而可知,C++動態綁定很棒,很強大。C++支持動態綁定和靜態綁定,符合下面三個條件,C++採用動態綁定,條件以下:
這裏還有一個概念——多態,List申明的時候,是經過pointer-to-A進行申明的,但實際是List能夠存儲各類不一樣的形狀,都屬於A,確是不一樣形態,因此叫多態。
因此,所謂的多態,虛函數,動態綁定,虛指針以及虛表,全部的故事都是同一件事情,真的瞭然於胸啊!😄
能夠再來看看以前講的Template method,這就是動態綁定的一個應用,子類CMyDoc調用父類的OnFileOpen函數,調用的時候會將子類的地址傳入到父類的函數中,也就是this pointer(成員函數都是一個默認參數,表明的就是this pointer,誰調用成員函數,誰的地址就是this pointer),在OnFileOpen中,全部的調用前面都會加上一個this pointer,而對於Serialize函數來講,它是符合上述咱們說的三個條件的,首先調用者是this,是指針,而後指向的是子類,向上轉型,調用的Serialize函數是虛函數,因此會使用動態綁定,調用CMyDoc的Serialize函數。這是很好的!
如今從彙編代碼的角度來看函數調用,初始化B類,講B類強制轉型爲A類,獲得a,調用a.vfunc1()函數,這裏是靜態綁定,由於是經過類調用函數,不是指針調用,彙編代碼也說明了這個問題,使用的是call xxx形式編譯函數。
而新建立了一個指針B,給的類型是指針A的類型,經過而後調用vfunc1函數,符合三個條件,是動態綁定,彙編代碼的形式也不同了,彙編表示看不懂,不過call一行連同上面幾行最後的表示在c語言中的描述確實是動態綁定的描述。另外,將b的地址賦給pa,從新調用vfunc1,同樣是動態綁定,與new B是同樣的。
#include <iostream> using namespace std; namespace fy2{ class A{ public: virtual void vfunc1() {cout << "A::vfunc1()" << endl;} private: int data1; }; class B : public A{ public: virtual void vfunc1() {cout << "B::vfunc1()" << endl;} private: int data2; }; void fy2_test(){ B b; A a = (A) b; a.vfunc1(); A* pa = new B; pa->vfunc1(); pa = &b; pa->vfunc1(); } } int main() { cout << "object model test:" << endl; fy2::fy2_test(); return 0; }
輸出結果:
object model test: A::vfunc1() B::vfunc1() B::vfunc1()
面向對象的筆記到此結束,深深感覺到了C++面向對象程序的power,fighting!