你好,C++(36)人蔘再好,也不能當飯吃!6.3 類是如何面向對象的

6.3  類是如何面向對象的

類做爲C++與面向對象思想結合的產物,做爲面向對象思想在C++中的載體,它的身上流淌着面向對象的血液。從類成員的構成到類之間的繼承關係再到虛函數,處處都體現着面向對象封裝、繼承和多態的三大特徵。函數

6.3.1  用類機制實現封裝

考慮這樣一個現實問題,學校中有多個老師,每一個老師的名字、年齡等屬性都各不相同,但這些老師都會備課上課,具備相同的行爲。那麼,咱們如何在程序中表現這些老師呢?老師的具體個體雖多,但他們都屬於同一類事物——老師。在C++中,咱們用類的概念來描述某一類事物,而抽象正是這個過程的第一道工序。抽象通常分爲屬性抽象和行爲抽象兩種。前者尋找一類事物所共有的屬性,好比老師們都有年齡、姓名等描述其狀態的數據,而後用變量將它們表達出來,好比用m_nAge變量表達年齡,用m_strName變量表達姓名;然後者則尋找這類事物所共有的行爲,好比老師都會備課、上課等,而後用函數將它們表達出來,好比用PrepareLesson()函數表達老師的備課行爲,用GiveLesson()函數表達其上課行爲。從這裏也能夠看出,整個抽象過程,是一個從具體(各個老師)到通常(變量和函數)的過程。spa

若是說抽象是將同類事物的共有屬性和行爲提取出來並將其用變量和函數表達出來,那麼封裝機制則是將它們捆綁在一塊兒造成一個完整的類。在C++語言中,咱們可使用6.2節中介紹的類(class)概念來封裝分析獲得的變量和函數,使其成爲類的成員,從而表現這類事物的屬性和行爲。好比老師這類事物就能夠封裝爲:設計

// 用Teacher類封裝老師的屬性和行爲
class Teacher
{
// 構造函數
public:
    // 根據名字構造老師對象
    Teacher(string strName) 
    {
            m_strName = strName;
    };

// 用成員函數描述老師的行爲
public:
    void PrepareLesson();      // 備課
    void GiveLesson();          // 上課
    void ReviewHomework();     // 批改做業
    // 其它成員函數…
    // 用成員變量描述老師的屬性
protected:
    string    m_strName;        // 姓名
    int        m_nAge;            // 年齡
    bool       m_bMale;           // 性別
    int        m_nDuty;           // 職務
private:
};

經過封裝,能夠將老師這類事物所共有的屬性和行爲緊密結合在Teacher類中,造成一個可重用的數據類型。從現實的老師到Teacher類,是一個從具體到抽象的過程,如今有了抽象的Teacher類,就能夠用它來定義某個對象,進而用這個對象來描述某位具體的老師,這又是一個從抽象到具體的過程。例如:3d

// 定義Teacher類對象描述學校中的某位陳老師
Teacher MrChen("ChenLiangqiao");
// 學校中的某位王老師
Teacher MrWang("WangGang");

雖然MrChen和MrWang這兩個對象都是Teacher類的對象,可是由於它們的屬性不一樣,因此能夠描述現實世界中的兩位不一樣的老師。指針

經過類的封裝,還能夠很好地實現對事物的屬性和行爲的隱藏。由於訪問控制的限制,外界是沒法直接訪問類的隱藏信息的,對於類當中的一些敏感數據,咱們能夠將其設置爲保護或私有類型,這樣就能夠防止其被意外修改,實現對數據的隱藏。另一方面,封裝好的類經過特定的外部接口(公有的成員函數)向外提供服務。在這個過程當中,外界看到的只是服務接口的名字和須要的參數,而並不知道類內部這些接口究竟是如何具體實現的。這就很好地對外界隱藏了接口的具體實現細節,而僅僅把外界最關心的服務接口直接提供給它。經過這種方式,類實現了對行爲的隱藏,如圖6-10所示。code

 

圖6-10  抽象與封裝對象

抽象與封裝,用來將現實世界的事物轉變成C++世界中的各個類,也就是用程序語言來描述現實世界。面向過程思想也有抽象這個過程,只是它的抽象僅針對現實世界中的過程,而面向對象思想的抽象不只包括事物的數據,同時還包括事物的行爲,更進一步地,面向對象利用封裝將數據和行爲有機地結合在一塊兒而造成類,從而更加真實地反映現實世界。抽象與封裝,完成了從現實世界中的具體事物到C++世界中類的過程,是將現實世界程序化的第一步,也是最重要的一步。blog

6.3.2  用基類和派生類實現繼承

在理解了類機制是如何實現面向對象思想的封裝特性以後,繼續分析上面的例子。在現實世界中,咱們發現老師和學生這兩類不一樣的事物有一些相同的屬性和行爲,好比都有姓名、年齡、性別,都能走路、說話、吃飯等。爲何不一樣的事物會有相同的屬性和行爲呢?這是由於這些特徵都是人類所共有的,老師和學生都是人類的一個子類別,因此都具備這些人類共同的屬性和行爲。像這種子類別和父類別擁有相同屬性和行爲的現象很是廣泛。好比小汽車、卡車是汽車的某個子類別,它們都具備汽車的共有屬性(發動機)和行爲(行駛);電視機、電冰箱是家用電器的某個子類別,它們都具備家用電器的共有屬性(用電)和行爲(開啓)。繼承

在C++中,咱們用類來表示某一類別的事物。既然父子兩個類別的事物可能有相同的屬性和行爲,這也就意味着父類和子類當中應該有大量相同的成員變量和成員函數。那麼,對於這些相同的成員,是否須要在父子兩個類中都定義一次呢?顯然不是。爲了描述現實世界中的這種父類別和子類別之間的關係,C++提供了繼承的機制。咱們把表示父類別的類稱爲基類或者父類,而把從基類繼承產生的表示子類別的類稱爲派生類或子類。繼承容許咱們在保持父類原有特性的基礎上進行更加具體的說明或者擴展,從而造成新的子類。例如,能夠說「老師是會上課的人」,那麼就可讓老師這個子類從人這個父類繼承,對於那些表現人類共有屬性和行爲的成員,老師類無需再次定義而直接從人類遺傳得到,而後在老師子類中再添加上老師特有的表示上課行爲的函數,經過繼承與發展,咱們就得到了一個既有人類的共有屬性和行爲,又有老師特有行爲的老師類。接口

所謂繼承,就是得到從父輩傳下來的財富。在現實世界中,這個財富多是金銀珠寶,也多是淳淳家風,而在C++世界中,這個財富就是父類的成員變量和成員函數。經過繼承,子類能夠輕鬆擁有父類的成員。而更重要的是,經過繼承能夠對父類的成員進行進一步的細化或者擴充來知足新的需求造成新的類。這樣,當複用舊有的類造成新類時,只須要從舊有的類繼承,而後修改或者擴充須要的成員便可。有了繼承機制,C++不只可以提升開發效率,同時也能夠應對不斷變化的需求,所以它也就成爲了消滅「軟件危機」的有力武器。

下面來看一個實際的例子,在現實世界中,有這樣一顆「繼承樹」,如圖6-11所示。

 

圖6-11  現實世界的繼承關係

從這棵「繼承樹」中能夠看到,老師和學生都繼承自人類,這樣,老師和學生就具備了人類的屬性和行爲,而小學生、中學生、大學生繼承自學生這個類,他們不但具備人的屬性和行爲,同時還具備學生的屬性和行爲。經過繼承,派生類不用再去重複設計和實現基類已有的屬性和行爲,只要直接經過繼承就擁有了基類的屬性和行爲,從而實現設計和代碼最大限度上的複用。

在C++中,派生類的聲明方式以下:

class 派生類名 : 繼承方式 基類名1, 繼承方式 基類名2…

{

    // 派生類新增長的屬性和行爲…

};

其中,派生類名就是咱們要定義的新類的名字,而基類名是已經定義的類的名字。一個類能夠同時繼承多個類,若是隻有一個基類,這種狀況稱爲單繼承,若是有多個基類,則稱爲多繼承,這時派生類能夠同時獲得多個基類的特徵,就如同咱們身上既有父親的特徵,同時也有母親的特徵同樣。可是,咱們須要注意的是,多繼承可能會帶來成員的二義性,由於兩個基類可能擁有同名的成員,若是都遺傳到派生類中,則派生類中會出現兩個同名的成員,這樣在派生類中經過成員名訪問來自基類的成員時,就不知道到底訪問的是哪個基類的成員,從而致使程序的二義性。因此,多繼承只在極少數必要的時候才使用,更多時候咱們使用的是單繼承。

跟類成員的訪問控制相似,繼承方式也有public、protected和private三種。不一樣的繼承方式決定了派生類如何訪問從基類繼承下來的成員,反映的是派生類和基類之間的關係:

(1) public。

public繼承被稱爲類型繼承,它表示派生類是基類的一個子類型,而基類中的公有和保護類型成員連同其訪問級別直接遺傳給派生類,不作任何改變。在基類中的public成員在派生類中也一樣是public成員,在基類中的protected成員在派生類中也是protected成員。public繼承反映了派生類和基類之間的一種「is-a」的關係,也就是父類別和子類別的關係。例如,老師是一我的(Teacher is-a Human),因此Teacher類應該以public方式繼承自Human類。 public所反映的這種父類別和子類別的關係在現實世界中很是廣泛,大到生物進化,小到組織體系,均可以用public繼承來表達,因此它也是C++中最爲常見的一種繼承方式。

(2) private。

private繼承被稱爲實現繼承,它把基類的公有和保護類型成員都變成本身的私有(private)成員,這樣,派生類將再也不支持基類的公有接口,它只但願能夠重用基類的實現而已。private繼承所反映的是一種「用…實現」的關係,若是A類private繼承自B類,僅僅是由於A類當中須要用到B類的某些已經存在的代碼但又不想增長A類的接口,並不表示A類和B類之間有什麼概念上的關係。從這個意義上講,private繼承純粹是一種實現技術,對設計而言毫無心義。

(3) protected。

protected繼承把基類的公有和保護類型成員變成本身的protected類型成員,以此來保護基類的全部公有接口再也不被外界訪問,只能由自身及自身的派生類訪問。因此,當咱們須要繼承某個基類的成員並讓這些成員能夠繼續遺傳給下一代派生類,而同時又不但願這個基類的公有成員暴露出來的時候,就能夠採用protected繼承方式。

在瞭解了派生類的聲明方式後,就能夠用具體的代碼來描述上面這棵繼承樹所表達的繼承關係了。

// 定義基類Human
class Human
{
// 人類共有的行爲,能夠被外界訪問,
// 訪問級別設置爲public級別
public:
    void Walk();               // 走路
    void Talk();               // 說話
    // 人類共有的屬性
    // 由於須要遺傳給派生類同時又防止外界的訪問,
    // 因此將其訪問級別設置爲protected類型
protected:
    string   m_strName;         // 姓名
    int      m_nAge;            // 年齡
    bool     m_bMale;           // 性別
private:   // 沒有私有成員
};

// Teacher跟Human是「is-a」的關係,
// 因此Teacher採用public繼承方式繼承Human
class Teacher : public Human
{
    // 在子類中添加老師特有的行爲
public:
    void PrepareLesson();      // 備課
    void GiveLesson();          // 上課
    void ReviewHomework();     // 批改做業
// 在子類中添加老師特有的屬性
protected:
    int    m_nDuty;             // 職務
private:
};

// 學生一樣是人類,public繼承方式繼承Human類
class Student : public Human
{
// 在子類中添加學生特有的行爲
public:
    void AttendClass();        // 上課
    void DoHomework();         // 作家庭做業
   // 在子類中添加學生特有的屬性
protected:
    int m_nScore;                // 考試成績
private:
};

// 小學生是學生,因此public繼承方式繼承Student類
class Pupil : public Student
{
// 在子類中添加小學生特有的行爲
public:
    void PlayGame();           // 玩遊戲
    void WatchTV();            // 看電視
public:
    // 對「作做業」的行爲從新定義
    void DoHomework();
protected:
private:
};

在這段代碼中,首先聲明瞭人(Human)這個基類,它定義了人這類事物應當具備的共有屬性(姓名、年齡、性別)和行爲(走路、說話)。由於老師是人的一種,是人這個類的具體化,因此咱們以Human爲基類,以public繼承的方式定義Teacher這個派生類。經過繼承,Teacher類不只直接具備了Human類中公有和保護類型的成員,同時還根據須要添加了Teacher類本身所特有的屬性(職務)和行爲(備課、上課),這樣就完成了對Human類的繼承和擴展,獲得的Teacher類是一個「會備課、上課的人類」。

// 定義一個Teacher對象
Teacher MrChen;
// 老師走進教室
// 咱們在Teacher類中並無定義Walk()成員函數,
// 這裏是經過繼承從基類Human中獲得的成員函數
MrChen.Walk();
// 老師開始上課
// 這裏調用的是Teacher本身定義的成員函數
MrChen.GiveLesson();

同理,咱們還經過public繼承Human類,同時增長了學生特有的屬性(m_nScore)和行爲(AttendClass()和DoHomwork()),定義了Student類。進而,又根據須要,以一樣的方式從Student類繼承獲得了更加具體的Pupil類來表示小學生。經過繼承,咱們能夠把整棵「繼承樹」完整清晰地表達出來。

仔細體會就會發現,整個繼承的過程就是類的不斷具體化、不斷傳承基類的屬性和行爲,同時發展本身特有屬性和行爲的過程。現實世界中的物種進化,經過子代吸取和保留部分父代的能力,同時根據環境的變化,對父代的能力作一些改進並增長一些新的能力來造成新的物種。繼承,就是現實世界中這種進化過程在程序世界中的體現。因此,類的進化也遵循着與之相似的規則:

(1) 保留基類的屬性和行爲。

繼承最大的目的就是複用基類的設計和實現,保留基類的屬性和行爲。對於派生類而言,不用本身白手起家,一切從零開始,只要經過繼承就直接成了擁有基類豐富屬性和行爲的「富二代」。在上面的例子中,派生類Teacher經過繼承Human基類,輕鬆擁有了Human類的全部公有和保護類型成員,這就像站在巨人的肩膀上,Teacher類只用不多的代碼就擁有了基類遺傳下來的姓名、年齡等屬性和走路、說話等行爲,實現了設計和代碼的複用。

(2) 改進基類的屬性和行爲。

既然是進化,派生類就要有優於基類的地方,這些地方就表如今派生類對基類成員的修改。例如,Student類有表示「作做業」這個行爲的DoHomework()成員函數,派生類Pupil原本直接繼承Student類也就一樣擁有了這個成員函數,可是,「小學生」作做業的方式是比較特殊的,基類定義的DoHomework()函數沒法知足它的需求。因此派生類Pupil只好從新定義了DoHomework()成員函數,從而根據本身的實際狀況對它作進一步的具體化,對它進行改寫以適應新的需求。這樣,基類和派生類都擁有DoHomework()成員函數,但派生類中的這個函數是通過改寫後的更具體的更有針對性的,是對基類的一種改進。

(3) 添加新的屬性和行爲。

若是進化僅僅是對原有事物的改進,那麼是遠遠不夠的。進化還須要一些「革命性」的內容才能產生新的事物。因此在類的繼承當中,派生類除了能夠改進基類的屬性和行爲以外,更重要的是添加一些「革命性」的新屬性和行爲使其成爲一個新的類。例如,Teacher類從Human類派生,它保留了基類的屬性和行爲,同時還根據須要添加了基類所沒有的新屬性(職務)和行爲(備課、上課),正是這些新添加的屬性和行爲,使它從本質上區別於Human類,完成了從Human到Teacher的進化。

很顯然,繼承既很好地解決了設計和代碼複用的問題——派生類繼承保留了基類的屬性和行爲,同時又提供了一種擴展的方式來輕鬆應對新的需求——派生類能夠改變基類的行爲同時根據須要添加新的屬性和行爲,而這正是面向對象思想的魅力所在。

既然繼承能夠帶來這麼多好處,不用費吹灰之力就能夠複用之前的設計和代碼,那麼是否是能夠在可以使用繼承的地方就都使用繼承,並且越多越好呢?

固然不是。人蔘再好,也不能當飯吃。正是由於繼承太有用,帶來了不少好處,因此每每會被初學者濫用,最後致使設計出一些「四不像」的怪物出來。在這裏,咱們要給繼承的使用定幾條規矩:

(1) 擁有相關性的兩個類才能發生繼承。

若是兩個類(A和B)絕不相關,則不能夠爲了使B的功能更多而讓B繼承A。也就是說,不能夠爲了讓「人」具備「飛行」的行爲,而讓「人」從「鳥」派生,那獲得的就再也不是「人」,而是「鳥人」了。不要以爲類的功能越多越好,在這裏,要奉行「多一事不如少一事」的原則。

(2) 不要把組合當成繼承。

若是類B有必要使用類A提供的服務,則要分兩種狀況考慮:

1)   B是A的「一種」。若在邏輯上B是A的「一種」(a kind of),則容許B繼承A。例如,老師(Teacher)是人(Human)的一種,是對人的特殊化具體化,那麼Teacher就能夠繼承自Human。

2)   A是B的「一部分」。若在邏輯上A是B的「一部分」(a part of),雖然二者也有相關性,但不容許B繼承A。例如,鍵盤、顯示器是電腦的一部分。

若是B不能繼承A,但A是B的「一部分」,B又須要使用A提供的服務,那又該怎麼辦呢?讓A的對象成爲B的一個成員,用A和其餘對象共同組合成B。這樣在B中就能夠訪問A的對象,天然就能夠得到A提供的服務了。例如,一臺電腦須要鍵盤的輸入服務和顯示器的輸出服務,而鍵盤和顯示器是電腦的一部分,電腦不能從鍵盤和顯示器派生,那麼咱們就把鍵盤和顯示器的對象做爲電腦的成員變量,一樣能夠得到它們提供的服務:

// 鍵盤
class Keyboard
{
public:
    // 接收用戶鍵盤輸入
    void Input()
   {
         cout<<"鍵盤輸入"<<endl;
    }
};

// 顯示器
class Monitor
{
public:
    // 顯示畫面
    void Display()
    {
          cout<<"顯示器輸出"<<endl;
    }
};

// 電腦
class Computer
{
public:
    // 用鍵盤、顯示器組合一臺電腦
    Computer( Keyboard* pKeyboard,
                Monitor* pMonitor )
    {
        m_pKeyboard = pKeyboard;
        m_pMonitor = pMonitor;
    }
    // 電腦的行爲
    // 其具體動做都交由其各個組成部分來完成
    // 鍵盤負責用戶輸入
    void Input()
    {
        m_pKeyboard->Input();
    }

    // 顯示器負責顯示畫面
    void Display()
    {
        m_pMonitor->Display();
    }

// 電腦的各個組成部分
private:
    Keyboard*  m_pKeyboard = nullptr;  // 鍵盤
     Monitor*   m_pMonitor = nullptr;  // 顯示器
// 其餘組成部件對象
};

int main()
{
     // 先建立鍵盤和顯示器對象
     Keyboard  keyboard;
    Monitor monitor;

    //  用鍵盤和顯示器對象組合成電腦
    Computer com(&keyboard,&monitor);

    // 電腦的輸入和輸出,實際上最終是交由鍵盤和顯示器去完成
    com.Input();
    com.Display();

    return 0;
}

在上面的代碼中,電腦這個類由Keybord和 Monitor兩個類的對象組成(固然,在具體實踐中還應該有更多組成部分),它的全部功能都不是它本身實現的,而是由它轉交給各個組成對象具體實現,它只是提供了一個統一的對外接口而已。這種把幾個類的對象結合在一塊兒構成新類的方式就是組合。雖然電腦沒有繼承鍵盤和顯示器,可是經過組合這種方式,電腦一樣得到了鍵盤和顯示器提供的服務,具有了輸入和輸出的功能。關於組合,還須要注意的是,這裏使用了對象指針做爲類成員變量來把各個對象組合起來,是由於電腦是一個能夠插拔的系統,鍵盤和顯示器都是能夠更換的。鍵盤能夠在這臺電腦上使用,也能夠在另外的電腦上使用,電腦和鍵盤的生命週期是不一樣的各自獨立的。因此這裏採用對象指針做爲成員變量,兩個對象能夠各自獨立地建立後再組合起來,也能夠拆分後另做他用。而若是遇到總體和部分密不可分的狀況,二者具備相同的生命週期,好比一我的和組成這我的的胳膊、大腿等,這時就該直接採用對象做爲成員變量了。例如:

// 胳膊
class Arm
{
public:
    // 胳膊提供的服務,擁抱
    void Hug()
    {
         cout<<"用手擁抱"<<endl;
    }
};

//
class Leg
{
public:
    // 腳提供的服務,走路
    void Walk()
    {
         cout<<"用腳走路"<<endl;
    }
};

// 身體
class Body
{
public:
    // 身體提供的服務,都各自交由組成身體的各個部分去完成
    void Hug()
    {
         arm.Hug();
    }

    void Walk()
    {
         leg.Walk();
    }
private:
    // 組成身體的各個部分,由於它們與Body有着共同的生命週期,
    // 因此這裏使用對象做爲類的成員變量
    Arm arm;
    Leg leg;
};

int main()
{
    // 在建立Body對象的時候,同時也建立了組成它的Arm和Leg對象
    Body body;
    // 使用Body提供的服務,這些服務最終由組成Body的Arm和Leg去完成
    body.Hug();
    body.Walk();

    // 在Body對象銷燬的同時,組成它的Arm和Leg對象也同時被銷燬
    return 0;
}
相關文章
相關標籤/搜索