日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C++學習之多態的使用詳解_C 語言

作者:之一Yo ? 更新時間: 2022-08-10 編程語言

前言

最近為了完成數據庫系統的實驗,又復習起了《C++ Primer》,上一次看這本巨著也是大二下的六月份,那時看面向對象程序編程這一章還云里霧里的,沒有領會多態的奧妙,學完 Java 之后回頭再看這一章發現對多態有了更好的理解。好記性不如爛筆頭,本篇博客將會對 C++ 的多態機制做一個不太詳細的總結,希望下一次不需要從頭再看一遍《C++ Primer》了 _(:з」∠)_。

多態

多態離不開繼承,首先來定義一個基類?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?不是虛函數或者我們是通過對象(而非引用或指針)進行的調用,則編譯器將產生一個常規函數調用。

原文鏈接:https://www.cnblogs.com/zhiyiYo/p/16387116.html

欄目分類
最近更新