C++面向對象繼承與多態

前言

經過前兩篇博文,我已經將多態的前提條件總結得七七八八了。這一篇開始正式展開講多態,以及咱們爲何要使用多態。編程

多態

什麼是多態

引用百度百科的定義:設計模式

多態(Polymorphism)按字面的意思就是「多種狀態」。在面嚮對象語言中,接口的多種不一樣的實現方式即爲多態。引用Charlie Calverts對多態的描述——多態性是容許你將父對象設置成爲一個或更多的他的子對象相等的技術,賦值以後,父對象就能夠根據當前賦值給它的子對象的特性以不一樣的方式運做(摘自「Delphi4 編程技術內幕」)。簡單的說,就是一句話:容許將子類類型的指針賦值給父類類型的指針。

個人理解是:子類能夠經過父類的指針或者引用,調用子類重寫父類的虛函數,以達到一個類型多種狀態的效果。函數

這聽起來好像沒有什麼,我能夠直接經過子類的對象調用成員函數不就好了,爲啥還要捨近求遠將其賦值到一個父類指針再調用呢?起初學習的時候我也不懂爲何,直到後來我遇到了一個很典型的例子才恍然大悟,這個例子我會在下面講到。學習

多態的條件

前面也零零散散地介紹了C++多態的條件,這裏總結一下:this

  • 須要有繼承
  • 須要使用父類的指針或引用
  • 父類須要有虛函數,子類要重寫父類的虛函數

須要上轉型是Java多態的條件,C++主要是經過使用父類的指針或者引用來實現的,也能夠認爲是一種上轉型吧。正是由於使用了父類的指針或者引用,才使得他可以調用子類的虛函數,而不是像上一篇的上轉型致使的靜態綁定,最終調用的是父類的虛函數。咱們經過如下代碼來回顧一下:spa

class base {
public:
    virtual void do_something() //有虛函數 {
        cout << "I'm base class" << endl;
    }
};

class derived : public base            //有繼承
{
public:
    void do_something() //子類重寫了父類的虛函數 {
        cout << "I'm derived class" << endl;
    }
};

void fun1(base &b) //父類的引用 {
    b.do_something();
}

void fun2(base *b) //父類的指針 {
    b->do_something();
}

void fun3(base b) {
    b.do_something();
}

int main() {
    derived d;
    fun1(d);    //I'm derived class
    fun2(&d);    //I'm derived class
    fun3(d);    //I'm base class
    return 0;
}

fun1()和fun2()實現的過程都是動態綁定的,即運行時才動態肯定要調用哪一個函數。那他到底是怎麼實現的?設計

動態綁定的原理

你們還記得虛類的對象是有一個vptr,多個同類對象的vptr指向同一個vtable。動態綁定就是經過這個vptr間接尋址來實現的。雖然子類對象被賦值到了父類的指針,可是對象的vptr是沒有改變的,他指向的仍是子類的vtable。 因此父類指針去調用某個虛函數的時候,就會去vtable裏面找函數入口,那找到的天然是子類的函數入口。因此他不是在編譯期間就肯定的,而是在代碼運行到那一行的時候才找到的函數入口。指針

那爲何只有指針或者引用才能達到這個效果呢?《深度探索C++對象模型》這本書對此有這樣一個解釋:code

一個pointer或一個reference之因此支持多態,是由於它們並不引起內存任何與類型有關的內存委託操做。會受到改變的,只有它們所指向內存的大小和解釋方式而已。

這樣讀起來有點拗口,簡單講就是指針或者引用的賦值並不會改變原對象內存裏的內容,他只會改變對內存大小及內容的解釋方式。舉個簡單的例子:我將int變量的地址賦值給了char型指針,char型指針才無論原來的變量是什麼,他對外只解釋一個字節的內容。對象

簡單的內存模型
同理可知,子類對象的內存內容並無發生改變,那麼對象的vptr仍是指向子類的vtable,因此調用的仍是子類的的成員函數。而簡單的上轉型並不會有這樣的效果,他會對內存進行從新分配。

另外說一下,只用C++有靜態綁定這個概念,其餘面向對象類的語言都是動態綁定。能夠看出C語言的知識是很細緻入微的。

爲何要使用多態

到此其實多態已經講完了,鋪墊了這麼多前置知識,其實多態就這麼一點點。我主要仍是想講講爲何要使用多態,只有知道了爲何,才能使咱們在設計代碼的時候考慮獲得如何運用這個知識點。咱們用一個遊戲的例子來講明爲何。

遊戲的描述以下:

  • 遊戲有一個英雄角色,角色屬性有生命(hp)和攻擊力(ack)
  • 英雄能夠對怪物進行攻擊,同時也會受到怪物的攻擊
  • 怪物屬性有生命(hp)和攻擊力(ack)
  • 怪物能夠對英雄進行攻擊,也會受到英雄的攻擊
  • 現階段有三種怪物:狼人,殭屍,女巫

咱們先來實現怪物類:

class wolf //狼人類 {
public:
    wolf(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
        return false;
    }

    bool attack(hero &hr) {
        return hr.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

class zombie {
public:
    zombie(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
        return false;
    }

    bool attack(hero &hr) {
        return hr.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

class witch {
public:
    witch(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
        return false;
    }

    bool attack(hero &hr) {
        return hr.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

而後咱們來實現英雄類:

class hero {
public:
    hero(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
    }

    bool attack(wolf &wf) {
        return wf.damage(this->ack);
    }

    bool attack(zombie &zb) {
        return zb.damage(this->ack);
    }

    bool attack(witch &wt) {
        return wt.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

咱們發現,一樣邏輯的attack()函數,咱們須要實現三次。若是後期遊戲要增添新的怪物,咱們還得繼續寫attack()函數。 這其實仍是一種面向過程的思想,並非說寫幾個類出來就是面向對象了。並且這也徹底不符合咱們程序猿的編程習慣,咱們程序猿不喜歡重複的東西。欸,這個時候多態就能發揮他的做用了。

咱們來定義怪物們的基類:

class monster {
public:
    virtual bool damage(int dm) = 0;
    virtual bool attack(hero &hr) = 0;
};

以前說了,咱們並不關心這個基類的虛函數具體是怎麼實現的,那麼咱們就能夠將其聲明爲純虛類。而後讓怪物都繼承這個基類,實現上面這兩個函數就能夠了。這樣咱們就能夠將hero類改形成這樣:

class hero {
public:
    hero(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
    }

    bool attack(monster &ms) //參數修改成monster類必定要用指針或者引用 {
        return ms.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

這樣代碼是否是就簡潔不少。並且根據多態的性質,不一樣的怪物會調用其各自的damage()函數。之後要是新增怪物,只要繼承和實現虛基類就行了,hero類並不須要進行修改。這就體現了面向對象編程的優點了,這還只是其中之一。

同理,要是有多種英雄,咱們一樣能夠抽象出一個英雄類的虛基類,而後派生出各式各樣的英雄,怪物類也不須要重複寫多個attack()函數。

有同窗仍是以爲怪物類的實現仍是重複度過高了,這沒有體現多態的優點啊。其實否則,前面說到每一個子類都應該重寫基類的虛函數,是由於不一樣的子類都應該有他的特別之處, 因此才叫派生嘛。若是子類和子類,或者子類和基類徹底同樣那就沒有必要繼承與派生了。

這裏重複度高只是由於代碼量小,我只是舉了個小小的例子,其實在真正的遊戲中不一樣怪物子類的attack()函數和damage()函數的內部細節應該是不同。好比不一樣的怪物有不一樣的攻擊特效,有不一樣的受擊效果,有不一樣的技能冷卻時間等等。這些細節都是經過子類去重寫基類的虛函數,才得以體現的。

總結

到此爲止,我所瞭解的繼承與多態算是總結完畢了。會簡單地封裝幾個類並非面向對象編程,只有完全理解了封裝、繼承與多態,面向對象編程纔算是入了個門。只有理解了這些,咱們才能開始學習設計模式,才能領悟到設計模式的精髓所在。學設計模式建議你們去看《大話設計模式》這本書,之後有時間我也會在個人博客裏總結一些設計模式。

相關文章
相關標籤/搜索