您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“C++多態如何使用”,內容詳細,步驟清晰,細節處理妥當,希望這篇“C++多態如何使用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
多態離不開繼承,首先來定義一個基類 Animal
,里面有一個虛函數 speak()
:
class Animal { public: Animal() = default; Animal(string name) : m_name(name) {} virtual ~Animal() = default; virtual void speak() const { cout << "Animal speak" << endl; } string name() const { return m_name; } private: string m_name; };
接著定義子類 Dog
,并重寫虛函數,由于構造函數無法繼承,所以使用 using
來 “繼承” 父類的構造函數。和父類相比,Dog
還多了一個 bark()
方法。
class Dog : public Animal { public: using Animal::Animal; // 可加上 override 聲明要重寫虛函數,函數簽名必須和基類相同(除非返回類自身的指針或引用) void speak() const override { cout << "Dog bark" << endl; } void bark() const { cout << "lololo" << endl; } };
我們在堆上創建一個 Dog
對象,并將地址賦給一個 Animal
類型的指針。由于指針指向的是個 Dog
對象,調用 speak()
方法時,實際上調用的是底層狗狗重寫之后的 speak()
方法,而不是基類 Animal
的 speak()
。也就是說編譯時不會直接確定要調用的是哪個 speak()
,要在運行時綁定。
Animal* pa = new Dog("二哈"); pa->speak(); // 調用的是 Dog::speak pa->Animal::speak(); // 強制調用基類的 speak
利用運行時綁定這一特點,我們將基類的析構函數定義為虛函數,這樣子類對象在析構的時候就能調用自己的虛函數了。
雖然 pa
指向的是一個 Dog
對象,但是不能使用 bark()
方法。因為 pa
是一個 Animal
類型的指針,在編譯時編譯器會跳過 Dog
而直接在 Animal
的作用域中尋找 bark
成員,結果發現并不存在此成員而報錯。
要實現向上轉型不止能用指針,引用同樣可以實現。但是如果寫成以下這種形式,實質上是調用了拷貝構造函數,會用 Dog
的基類部分來初始化 Animal
對象,和向上轉型沒有任何關系,之后調用的就是底層 Animal
對象的 speak()
方法:
Dog dog("二哈"); Animal animal = dog; animal.speak(); // 調用的是 Animal::speak
要想調用底層 Dog
對象的 bark()
方法,我們需要將 pa
強轉為 Dog
類型的指針。一種方法是使用 static_cast
進行靜態轉換,另一種這是使用 dynamic_cast
進行運行時轉換。相比于前者,dynamic_cast<type *>
轉換失敗的時候會返回空指針,而 dynamic_cast<type &>
則會報 bad_cast
錯誤,因此更加安全。
Dog* pd_ = static_cast<Dog *>(pa); pd_->bark(); if (Dog* pd = dynamic_cast<Dog*>(pa)) { pd->bark(); } else { cout << "轉換失敗" << endl; }
子類的作用域是嵌套在父類里面的,在子類的對象上查找一個成員時,會現在子類中查找,如果沒找到才回去父類中尋找。由于作用域的嵌套,會導致子類隱藏掉父類中的同名成員。比如下述代碼:
class Animal { public: virtual void speak() const { cout << "Animal speak" << endl; } }; class Dog : public Animal { public: // void speak() const override { cout << "Dog speak" << endl; } void speak(string word) const { cout << "Dog bark: " + word << endl; } }; int main(int argc, char const* argv[]) { Animal* pa = new Dog(); Dog* pd = new Dog(); // pd->speak(); 報錯 pd->speak("666"); // Dog::speak 隱藏了 Animal::speak return 0; }
我們在父類中定義了一個虛函數 void speak()
,子類中沒有重寫它,而是定義了另一個同名但是參數不同的函數 void speak(string word)
。這時候子類中的同名函數會隱藏掉父類的虛函數,如果寫成 pd->speak()
,編譯器會先在子類作用域中尋找名字為 speak
的成員,由于存在 speak(string word)
,它就不會接著去父類中尋找了,接著進行類型檢查,發現參數列表對不上,會直接報錯。如果用了 VSCode 的 C/C++ 插件,可以看到參數列表確實只有一個,沒有提示有重載的同名函數。
要想通過調用基類的 speak()
方法,有兩種方法:
向上轉型,使用基類的指針 pa
來調用 pa->speak()
,由于子類沒有重寫虛函數,所以在動態綁定時會調用父類的虛函數;
使用作用域符強制調用父類的虛函數:pd->Animal::speak()
《C++ Primer》對名字查找做了一個非常好的總結:
理解函數調用的解析過程對于理解 C++ 的繼承至關重要,假定我們調用 p->mem()
(或者 obj.mem()
),則依次執行以下4個步驟:
1.首先確定 p
(或 obj
) 的靜態類型。因為我們調用的是一個成員,所以該類型必 然是類類型。
2.在 p
(或 obj
) 的靜態類型對應的類中查找 mem
。如果找不到,則依次在直接基類中不斷查找直至到達繼承鏈的頂端。如果找遍了該類及其基類仍然找不到,則編譯器將報錯。
3.一旦找到了 mem
,就進行常規的類型檢查以確認對于當前找到的 mem
,本次調用是否合法。
4.假設調用合法,則編譯器將根據調用的是否是虛函數而產生不同的代碼:
如果mem是虛函數且我們是通過引用或指針進行的調用,則編譯器產生的代 碼將在運行時確定到底運行該虛函數的哪個版本,依據是對象的動態類型。
反之,如果 mem
不是虛函數或者我們是通過對象(而非引用或指針)進行的調用,則編譯器將產生一個常規函數調用。
讀到這里,這篇“C++多態如何使用”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。