C++ 之多態總結
前言
最近為了完成資料庫系統的實驗,又複習起了《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++ Primer》中對於上述內容有更加詳細的講解,這裡不過是拾其牙慧罷了。看了這一章之後最大的感受就是:
你看 100 遍她的影片,她不是你的,你看 100 遍 《C++ Primer》,知識就是你的
以上~~