標籤:
最近在看 C++,再細讀關於物件導向的一些知識點,好記性不如爛筆頭,就歸類總結一下。
物件導向中,繼承是非常重要的基礎概念。從已有的類派生出新的類,就可對原有的類進行擴充和修改。
稱已有的類為基類,繼承出來的類叫做衍生類別。
什麼時候使用繼承呢? 如果兩個對象是is-a關係,則可以用繼承。
比如水果與蘋果。蘋果 (is-a) 水果,這時候 水果是基類,蘋果則是水果的衍生類別。
如果不是(is-a)關係,最好不要用繼承。比如下面的關係例子。
has-a 午餐 (has-a) 水果,但是午餐 !(is- a )水果。所以不合適繼承
is-like-a 敵人(is-like-a)豺狼,但是敵人 !(is- a )豺狼。所以不適合繼承。
is-implemented-as-a 數組(is-implemented-as-a)堆棧,但是 數組 !(is- a )堆棧。所以不適合繼承。
uses-a 電腦(uses-a)印表機,但是 電腦 !(is- a )印表機。所以不適合繼承。
繼承實現很簡單,但是要管理好繼承類之間的關係,卻是件比較複雜的事情。
涉及到構造解構函式,虛函數&虛函數表,靜態聯編&動態聯編,抽象基類,動態記憶體分配等。
如果不瞭解這些機制,設計的類之間就會出現一些和自己原有意圖違背的運行錯誤,以及記憶體流失。
繼承的基本實現非常簡單。文法就是定義類時,定義類class MyClass的後面 接上": 修飾符 基類名"即可。
修飾符有 public, private, protect 3種。這3個修飾符分別表示了C++的3種繼承方式:公有繼承,私人繼承,保護繼承。
比如SimpleClass繼承了BaseClass, 「class SimpleClass : public BaseClass」並且修飾符為public,表明是公有繼承。
公有繼承也是最常用的方式。
關於修飾符,遵循一個基本規則,不論哪種繼承方式,基類的私人成員,私人函數在衍生類別中都是不可見的。
想訪問他們,只能通過基類提供公有或者保護方法去訪問。
通過3種繼承方式,原有基類的成員訪問屬性也會發生變化。
公有繼承:衍生類別公有繼承基類後,基類的成員和函數的訪問限制在衍生類別中不變。
基類原來是public,到了衍生類別裡還是public。
私人繼承:衍生類別私人繼承基類後,基類的成員和函數的訪問限制在衍生類別全部變成私人。
所以如果私人繼承一個基類,就相當於完全隱藏了基類中的所有成員和函數。
保護繼承:衍生類別私人繼承基類後,基類的成員和函數的public訪問限制在衍生類別全部變成protect。
基類中的private還是繼續保持private。
關於protect屬性的注意點,類資料成員一般推薦用private,而不用protect或者public。
理由是用protect的話,衍生類別可以直接存取修改基類的資料成員。
成員函數用protect限定比較有用,使此成員函數只對衍生類別公開,對公眾保持隱秘。
衍生類別需要有自己的建構函式,如果你沒有看到建構函式,那隻是編譯器悄悄用預設的建構函式了。
衍生類別一定會調用基類的建構函式! 那麼調用基類的那個建構函式呢? 答案很簡單,你指定哪個就是哪個,如果你太懶不指定,那就調用預設的建構函式。
那如何指定呢?用成員初始化列表句法即可。
比如有一個藝術家類,記錄藝術家的姓名,年齡。藝術家類派生出一個畫家類。
畫家除了有藝術家的姓名,年齡的基本屬性外,還設定一個畫作類別屬性,表示畫家最擅長的畫是油畫,中國畫,水彩畫之中的某一種。
#ifndef __CJiaJia__Artist__#define __CJiaJia__Artist__#include <iostream>
using namespace std;enum {LIM = 20};class Artist{private: char firstname[LIM]; //所有藝術家都應該有一個姓名 char lastname[LIM]; unsigned int age; //所有藝術家都有自己的年齡 public: Artist (const char *fn = "none", const char *ln = "none", unsigned int age = 5); //建構函式 void showName() const; //顯示藝術家的姓名 int getAge() const { return age; }; //獲得藝術家的年齡 void setAge(int age) { this->age = age; }; //設定藝術家的年齡};class Painter : public Artist{private: char category[LIM]; //顯示畫家的畫作種類。如水墨畫,油畫,水彩畫等 public: Painter(const char *ct, const char *fn, const char *ln, unsigned int age); //建構函式1 Painter(const char *ct, const Artist & art); //建構函式2 char getCategory() const; //取得畫家的繪畫種類 void setCategory(const char *ct); //設定畫家的繪畫種類};
#endif /* defined(__CJiaJia__Artist__) */
#include "Artist.h"Artist::Artist (const char *fn, const char *ln, unsigned int ag){ strncpy(firstname, fn, LIM - 1); firstname[LIM - 1] = ‘\0‘; strncpy(lastname, ln, LIM - 1); lastname[LIM - 1] = ‘\0‘; age = ag;}void Artist::showName() const{ cout << firstname <<" "<< lastname<< endl;}Painter::Painter(const char *ct, const char *fn, const char *ln, unsigned int age): Artist( fn, ln, age){ strncpy(category, ct, LIM - 1); category[LIM - 1] = ‘\0‘;}Painter::Painter(const char *ct, const Artist & art): Artist(art){ strncpy(category, ct, LIM - 1); category[LIM - 1] = ‘\0‘;}
藝術家類的構造方法裡,指定了藝術家的姓名,年齡。畫家類同樣擁有姓名,年齡,還特別記錄了擅長的畫作類別。
畫家類的建構函式1裡,用初始化成員列表顯式調用藝術家類的建構函式 Artist( fn, ln, age )。
Painter::Painter(const char *ct, const char *fn, const char *ln, unsigned int age): Artist( fn, ln, age)
畫家類的建構函式2裡,將調用Artist類的複製建構函式。在這個例子裡Artist類沒有顯式定義複製建構函式,所以調用其預設的複製建構函式進行淺拷貝即可。
關於複製建構函式,如果Artist類裡面有成員變數指標通過new申請了記憶體,則需要顯式定義Artist類的複製建構函式以對此成員進行深拷貝。
Painter::Painter(const char *ct, const Artist & art): Artist(art)
衍生類別可以使用基類的非私人方法。
Painter chinesePainter("中國畫", "潘", "天壽", 74);
chinesePainter.showName();
基類指標可以在不進行顯式類型轉換的情況下指向衍生類別對象。基類引用可以在不進行顯式類型轉換的情況下引用衍生類別對象。
這叫向上強制轉換。畫家是藝術家,但是藝術家不是畫家。這和現實邏輯是一樣的。
Painter chinesePainter("中國畫", "潘", "天壽", 74);
Artist & rt = chinesePainter;
Artist * pt = &chinesePainter;
rt.showName();pt->showName();
同一個方法在衍生類別和基類中可以有不同的行為,取決於調用該方法的對象。
C++有兩種重要的機制實現多態公有繼承。
- 在衍生類別中重新定義基類的方法
- 使用虛方法
比如上面的painter類,聲明一個基類的方法。
void showName() const; //畫家類也定義一個顯示姓名的方法,同時顯示畫家的畫作類別
void Painter::showName() const;
{
cout << firstname<< " "<< lastname<< "擅長" << category << endl;
}
如果不showName不指定為virtual 方法,那麼下面代碼運行情況如下:
Artist chineseArtist("齊", "白石", 93);
Painter chinesePainter("中國畫", "潘", "天壽", 74);
Artist & art1_ref = chineseArtist;
Artist & art2_ref = chinesePainter;
//如果showName不是vitrual方法
art1_ref.showName(); //使用 Artist::showName()
art2_ref.showName(); //使用 Artist::showName()
//如果showName是vitrual方法
art1_ref.showName(); //使用 Artist::showName()
art2_ref.showName(); //使用 Painter::showName()
如果一個方法是virtual虛方法,程式會根據引用或者指標指向的對象的類型來選擇方法。
如果一個方法不是virtual虛方法,程式會根據引用或者指標的類型來選擇方法。
在基類中如果將方法聲明為虛方法,在衍生類別中即使不明確指定該方法為虛方法,也是虛方法。
衍生類別方法中若要調用基類的方法,加上域限定修飾符和該方法名調用即可。
按照慣例,基類應該包含一個虛擬解構函式!理由是為了調用相應物件類型的解構函式,然後基類的解構函式會被自動調用。
如果衍生類別包含了執行某些操作的解構函式,則基類必須有虛擬解構函式,即使該解構函式不執行任何操作。
程式調用函數時,編譯器決定使用哪個執行代碼塊。根據函數調用去執行特定的代碼塊被稱為聯編。
在編譯過程中,就能完成的聯編稱為靜態聯編。
但是由於c++裡有虛函數的存在,編譯時間期不能確定使用哪個函數,編譯器必鬚生成在程式運行時選擇正確的虛方法的代碼,
這被稱為動態聯編。
什麼時候用動態聯編,什麼時候用靜態聯編呢? 非虛方法用靜態聯編,虛方法用動態聯編。
大多數時候,動態聯編很好,它可以讓程式選擇為特定類型設計的方法。
但是編譯器預設還是使用靜態聯編。理由在於效率,為了使程式在運行階段決心決策,編譯器必須採取一些方法來跟蹤基類指標(虛函數表),增加額外的開銷。
大神說C++的指導原則之一就是,不要為不使用的特性付出代價(記憶體或者處理時間)。
所以如果要在衍生類別中重新定義基類的方法,則將它設定為虛方法,否者則應該設為非虛方法。
編譯器處理虛函數的方法,就是給每個對象添加一個隱藏成員。隱藏成員中儲存了一個指向函數地址數組的指標。
這個數組就稱為虛函數表(virtual function table, vtbl)。虛函數表中儲存了為類對象進行聲明的虛函數的地址。
可以看出虛函數定義得越多,數組就越大。調用虛函數時,編譯器會到表中去尋找函數的地址。
如果在畫家類中重新了如下的方法 void showName(int type) const ,那麼Artist類的showName() 將被隱藏。
畫家類調用showName()時,就會出現編譯錯誤。
void showName(int type) const; //畫家類也定義一個顯示姓名的方法,同時顯示畫家的畫作類別
void Painter::showName(int type) const;
{
cout << firstname<< " "<< lastname<< "擅長" << category << endl;
}
所以總結2條經驗規則:
1. 重新定義繼承的方法應該和基類的原型完全相同。 例外情況是函數的傳回值如果是基類的引用或指標,將其修改為指向衍生類別的引用或指標是OK的。
這個特性叫傳回型別協變( covariance of return type)。
2. 如果基類中的函數重載了,衍生類別中又想重新定義它們的實現,那麼應在衍生類別中重新定義所有的基類版本。
如果偷懶只定義一個版本,另外的重載版本將被隱藏。
定義抽象類別的原因是,一些雖然是is-a的關係通過繼承出來,解決問題的效率卻不高。
比如圓和橢圓。圓只需要半徑值 就可以描述大小和形狀,不需要長半軸a和短半軸b。當然也可以通過將同一個值賦給成員a和b來照顧這種情況,但是導致資訊冗餘沒有必要。
這時候可以把圓和橢圓的共性放到一個ABC中,從該ABC派生出圓和橢圓類。
從文法上來說,至少有一個純虛函數的類即為抽象類別,不能建立抽象類別的執行個體對象。
純虛函數就是在虛函數結尾加個 =0 ,即表明該函數是純虛函數。
比如virtual double Area() const = 0;
如果類成員通過new進行初始化,那麼則要定義相關的複製建構函式,重載賦值操作符=。
暫時就寫這麼點了。。。
C++讀書筆記 關於繼承