繼承和多態
- 繼承
- 繼承的權限
- 繼承的子父類訪問
- 派生類的默認成員函數
- 菱形繼承(C++獨有)【了解】
- 虛擬繼承
- 什么是菱形繼承?菱形繼承的問題是什么?
- 什么是菱形虛擬繼承?如何解決數據冗余和二義性的
- 繼承和組合的區別?什么時候用繼承?什么時候用組合?
- 多態
- 構成多態的條件
- 多態的原理
- 虛函數表的打印
- 虛函數的重寫
- 虛函數重寫的第一個例外--- 析構函數的重寫
- 虛函數重寫的第二個例外---協變
- 重載、覆蓋(重寫)、隱藏(重定義)的對比
- 重寫和隱藏的詳細解釋
- 抽象類
- C++11的override和final
- final
- override
- 面試問題
- inline函數可以是虛函數嗎?
- 靜態成員可以是虛函數嗎?
- 構造函數可以是虛函數嗎?
- 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
- 對象訪問普通函數快還是虛函數更快?
- 虛函數表是在什么階段生成的,存在哪的?
- 什么是抽象類?抽象類的作用?
繼承
繼承的權限
繼承的子父類訪問
派生類的默認成員函數
菱形繼承(C++獨有)【了解】
iostream就是菱形繼承 可以去查庫
虛擬繼承
虛擬繼承可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student
和
Teacher
的繼承Person
時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地
方去使用
當一個類從多個類中繼承時,這些類又共同繼承自另一個基類,這時可以使用虛擬繼承來確保基類的共享實例。具體來說,就是在派生類聲明中使用virtual
關鍵字來繼承基類,這樣在進一步的派生中,基類的成員就會被共享,而不是重復復制。
例如,如果Father和Mother類都虛擬繼承自GrandParent
類,那么當GrandSon
類繼承Father
和Mother
時,GrandSon
對象中只會有一個GrandParent
的實例,這樣就避免了數據冗余。同時,由于虛擬基類的引入,對于基類成員的訪問也變得明確,解決了二義性問題。
class GrandParent {
public:void sayHello() {std::cout << "Hello from GrandParent!" << std::endl;}
};class Father : virtual public GrandParent {
};class Mother : virtual public GrandParent {
};class GrandSon : public Father, public Mother {
};int main() {GrandSon son;son.sayHello(); // 輸出 "Hello from GrandParent!"return 0;
}
什么是菱形繼承?菱形繼承的問題是什么?
菱形繼承是一種多繼承的特殊情況,它涉及四個類形成一個菱形結構。
在菱形繼承中,存在一個基類,兩個派生類繼承這個基類,然后另一個類同時繼承這兩個派生類。這種繼承方式在類的層次結構圖中看起來像一個菱形,因此得名。
菱形繼承的主要問題是數據冗余和二義性。
由于最底層的派生類繼承了兩個基類,而這兩個基類又繼承了同一個基類,所以會造成最頂部基類的兩次調用。這會導致相同數據的重復存儲,即冗余性。更重要的是,當訪問某個繼承自基類的屬性或方法時,會產生歧義,因為不清楚應該訪問哪個派生類中的版本,這就是所謂的二義性。
總而言之,菱形繼承是多繼承中特有的一種復雜情況,在設計類的繼承關系時應謹慎使用,以避免引起數據冗余和二義性問題。
什么是菱形虛擬繼承?如何解決數據冗余和二義性的
菱形虛擬繼承是一種特殊的多繼承方式,它通過虛擬基類來解決菱形繼承中的數據冗余和二義性問題。
在C++中,菱形虛擬繼承是通過使用關鍵字virtual
來實現的。當一個類從多個類中繼承時,這些類又共同繼承自另一個基類,這時可以使用虛擬繼承來確保基類的共享實例。具體來說,就是在派生類聲明中使用virtual
關鍵字來繼承基類,這樣在進一步的派生中,基類的成員就會被共享,而不是重復復制。
例如,如果Father
和Mother
類都虛擬繼承自GrandParent
類,那么當GrandSon
類繼承Father
和Mother
時,GrandSon
對象中只會有一個GrandParent
的實例,這樣就避免了數據冗余。同時,由于虛擬基類的引入,對于基類成員的訪問也變得明確,解決了二義性問題。
總的來說,雖然菱形虛擬繼承可以解決這些問題,但它也會增加代碼的復雜性。因此,在設計類的繼承結構時,應當謹慎考慮是否真的需要使用多繼承和虛擬繼承,以及它們帶來的復雜性和可能的性能影響。
繼承和組合的區別?什么時候用繼承?什么時候用組合?
下面以表格形式對比繼承和組合的區別以及它們的適用場景:
特性 | 繼承 | 組合 |
---|---|---|
定義 | 繼承 是一種從現有類派生新類的關系。 | 組合 是指一個類包含另一個類的實例。 |
耦合性 | 通常較高,因為子類與父類緊密相關。 | 較低,因為類之間通過接口進行交互。 |
封裝性 | 可能破壞封裝性,因為子類能訪問父類保護成員。 | 維護良好的封裝性,只通過接口交互。 |
代碼重用 | 允許子類重用父類的代碼和行為。 | 通過聚合或包含實現代碼重用。 |
多態性 | 支持,子類可以覆蓋或擴展父類方法。 | 不直接支持,需要通過其他機制實現。 |
設計靈活性 | 修改父類可能會影響所有子類。 | 更靈活,整體與部分獨立變化。 |
使用場景 | 適用于“是一個”關系(如貓是動物)。 | 適用于“有一個”關系(如車有引擎)。 |
示例 | Dog 繼承自Mammal ,Mammal 繼承自Animal 。 | Car 包含Engine 對象作為其組成部分。 |
何時使用繼承:
- 當你想表達一種類型層級,例如,所有的貓都是哺乳動物,所有的哺乳動物都是動物。
- 當子類需要父類的屬性和方法,并且可能還需要在子類中添加額外的特性或重寫父類的方法。
何時使用組合:
- 當你想表達的是聚合關系,例如,一輛車有一個引擎,但車不是引擎的一種類型。
- 當你希望保持類之間的松耦合,使得一個類的內部實現可以獨立于使用它的類而變化。
在實際的軟件開發中,組合通常被認為是比繼承更有優勢的設計選擇,因為它提供了更好的靈活性和封裝性。然而,在某些情況下,繼承仍然是合適的,特別是在表示自然的層次關系時。
多態
構成多態的條件
繼承中要構成多態還有兩個條件:
- 必須通過基類的指針或者引用調用虛函數
- 被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫
多態的原理
虛函數表的打印
class Base {
private:int _b=1;
public:Base():_b(10){++_b;}virtual void fun1() {cout << "Base::fun1" << endl;}virtual void fun2() {cout << "Base::fun2" << endl;}void fun3() {cout << "Base::fun3" << endl;}
};class Derive :public Base {
private:int _d = 2;
public:virtual void fun1() {cout << "Derive::fun1" << endl;}virtual void fun4() {cout << "Derive::fun4" << endl;}
};typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR* table) {for (int i = 0;table[i] != nullptr;++i) {printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}
int main() {Base b;Derive d;PrintVFTable((*(VF_PTR**)&b));PrintVFTable((*(VF_PTR**)&d));return 0;
}
虛函數的重寫
虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的
返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數。
虛函數重寫的第一個例外— 析構函數的重寫
基類與派生類析構函數的名字不同
如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,
都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,
看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處
理,編譯后析構函數的名稱統一處理成destructor
虛函數重寫的第二個例外—協變
基類與派生類虛函數返回值類型不同
派生類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指
針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱為協變。(了解)
重載、覆蓋(重寫)、隱藏(重定義)的對比
在C++中,重載(Overload)、覆蓋(Override,也稱為重寫)和隱藏(Hide,也稱為重定義)是三種不同的函數關系。它們的區別可以通過下表進行總結:
概念 | 作用域 | 參數列表 | 返回類型 |
---|---|---|---|
重載 | 同一作用域 | 必須不同 | 可相同也可不同 |
覆蓋/重寫 | 派生類與基類之間 | 相同 | 必須相同(C++11起,返回類型也可以被協變) |
隱藏 | 不同作用域(如基類與派生類) | 可以相同,也可以不同 | 無特定要求 |
具體解釋如下:
- 重載:
- 作用域:發生在同一作用域內,通常是同一個類中。
- 參數列表:同名函數必須有不同的參數列表(參數類型、個數或順序至少有一項不同)。
- 返回類型:可以相同,也可以不同。
- 覆蓋/重寫:
- 作用域:發生在基類與派生類之間。
- 參數列表:派生類中的函數必須與基類中的虛函數有完全相同的參數列表。
- 返回類型:從C++11開始,返回類型可以是相同的,或者是派生類類型的派生類(協變返回類型)。
- 隱藏:
- 作用域:發生在不同作用域,例如基類與派生類中的非虛函數。
- 參數列表:同名函數的參數列表可以相同,也可以不同。
- 返回類型:沒有特定的要求。
綜上所述,重載允許在同一作用域內有多種接受不同參數的同名函數;覆蓋/重寫是指派生類重新定義了基類的虛函數,通常用于實現多態;而隱藏則是當派生類中的函數與基類中的函數同名時,無論參數列表是否相同,基類中的函數都會被隱藏。理解這些概念對于編寫正確的C++面向對象程序至關重要。
重寫和隱藏的詳細解釋
重寫(Overriding):
當你在派生類中定義一個與基類中同名且函數簽名(包括參數類型和返回類型)完全相同的虛函數時,你實際上是在提供一個新的實現。當通過基類的指針或引用調用這個函數時,C++運行時將動態地(在程序運行時)決定執行基類的版本還是派生類的版本,這是多態的一個特征。
class Base {
public:virtual void doSomething() {cout << "Base's doSomething" << endl;}
};class Derived : public Base {
public:virtual void doSomething() override { // 重寫基類的方法cout << "Derived's doSomething" << endl;}
};
在這個例子中,Derived
類重寫了Base
類的虛函數doSomething
。
隱藏(Hiding):
當派生類定義了一個與基類中同名的成員函數,哪怕是參數個數或類型不同,或者不是虛函數,基類的那個成員函數在派生類的對象上就無法直接訪問了。這被稱為隱藏。這不是多態的表現,而是簡單的名字覆蓋,這種情況下不會有運行時的動態調用。
class Base {
public:void doSomething() {cout << "Base's doSomething" << endl;}
};class Derived : public Base {
public:void doSomething(int x) { // 隱藏了基類的doSomethingcout << "Derived's doSomething with int" << endl;}
};
在這個例子中,Derived
類隱藏了Base
類的doSomething
函數。
總結一下:
- 重寫是多態的一個體現,重寫必須涉及到虛函數,函數簽名必須相同,C++通過虛函數表來實現運行時的動態綁定。
- 隱藏發生在派生類聲明了一個與基類同名的函數后(而不管簽名是否相同),此時通過派生類的對象將不能訪問到基類中被同名函數隱藏的成員,除非顯式指定作用域。隱藏不涉及虛函數或運行時的動態綁定。
抽象類
在虛函數的后面寫上 =0 ,則這個函數為純虛函數。**包含純虛函數的類叫做抽象類(也叫接口
類),抽象類不能實例化出對象。**派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生
類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒適" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
void Test()
{
Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}
C++11的override和final
final
final:修飾虛函數,表示該虛函數不能再被重寫
我的理解是這個虛函數是父類特有的功能,不能被子類所繼承。
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒適" << endl;}//這里會報錯說不能繼承
};
override
override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯。
我的理解是這功有點雞肋,就是你在子類繼承后面寫override,override會幫你檢查該函數是否是需要虛函數重寫。
面試問題
inline函數可以是虛函數嗎?
答:可以,不過編譯器就忽略inline屬性,這個函數就不再是inline,因為虛函數要放到虛表中去。
靜態成員可以是虛函數嗎?
答:不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
構造函數可以是虛函數嗎?
答:不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。
析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
答:可以,并且最好把基類的析構函數定義成虛函數。
對象訪問普通函數快還是虛函數更快?
答:首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。
虛函數表是在什么階段生成的,存在哪的?
答:虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。
什么是抽象類?抽象類的作用?
答:參考(3.抽象類)。抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關系。