C++-多態

多態

多態的概念

通俗的來講,就是多種形態。具體點就是去完成某個行爲,當不同的對象去完成時會產生不同的狀態。

比如:買票這個行爲,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是

買票。

多態的定義及實現

多態的構成條件

多態是在不同繼承關係的類對象,去調用同一個函數,產生了不同的行爲。比如Student繼承了Person。Person對象買票全價,Student對象買票半價。

所以在多態的繼承中要構成多態還有兩個條件

1、必須通過基類的指針或者引用調用虛函數

2、被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫

在這裏插入圖片描述

虛函數

被virtual修飾的類成員函數稱爲虛函數

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全價買票"<<endl;
    }
};

虛函數的重寫

在派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型,參數列表,函數名字完全相同),稱子類的虛函數重寫了基類的虛函數。

class Person {
public:
	virtual void BuyTicket() { cout << "買票-全價" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
虛函數重寫有兩個例外

1、協變:基類與派生類的返回值類型不同

派生類重寫基類函數時,與基類虛函數返回值不同。即基類虛函數返回基類對象的指針或者引用派生類虛函數返回派生類對象的指針或者引用

2、析構函數的重寫:基類與派生類的命名不同

如果基類的析構函數爲虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然名字不同,但是在編譯過程中,編譯器做了特殊的處理,編譯後析構函數的名稱統一處理成destructor。

C++11中的override和final

C++對函數重寫比較嚴格,但是如果函數名中的個別單詞寫反,編譯器是無法識別出來的,通過override和final來幫助用戶檢測是否重寫

final:修飾虛函數,該虛函數不能再被繼承

class Car
{
public:
	virtual void Drive() final {}
};

override:檢查派生類是否重寫了基類的某個虛函數,如果沒有則報錯

class Car{
public:
	virtual void Drive(){}
};
class Benz :public Car {
public:
	virtual void Drive() override {cout << "Benz-舒適" << endl;}
};

重載、覆蓋(重寫)、隱藏(重定義)的區別

重載:在同一作用域中,可以有一組具有相同函數名,不同參數列表的函數,這組函數被稱爲重載函數。重載函數通常用來命名一組功能近似的h函數,這樣減少了函數的命名衝突,避免了命名空間的污染。

覆蓋(重寫):在派生類中有一個完全跟基類相同的虛函數。函數名,參數列表,返回值什麼都完全相同,稱子類的虛函數重寫了父類的虛函數。

隱藏(重定義):子類和父類中有同名函數,子類成員將屏蔽父類對成員函數的直接訪問,這種情況叫隱藏,也叫重定義。在子類成員函數中,可以使用基類::基類成員顯式訪問。

抽象類

在虛函數的後面寫上=0,則這個函數爲純虛函數。包含純虛函數的類叫做抽象類。抽象類不能實例化出對象,派生類繼承後也不能實例化出對象,只有重寫純虛函數。純虛函數規範了派生類必須重寫。

class Car
{
public:
    virtual Drive() = 0;
};
接口繼承和實現繼承

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是重寫,爲了達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。

多態的原理

虛函數表

class Base
{
public:
    virtual fun1()
    {
        cout<<"fun1()"<<endl;
    }
private:
    int _b = 1;
};

有上面這麼一個類,那麼它的sizeof()是多大?根據之前的經驗,我們會認爲是4bytes,但是因爲有虛函數的存在,答案爲8bytes。這是因爲有虛函數表存在。

虛函數的地址存放在虛函數表中,類的對象內部會有指向類內部的虛表地址的指針。通過這個指針調用虛表。虛函數的調用會被編譯器轉換爲對虛函數表的訪問。

在這裏插入圖片描述

對象中的這個指針_vfptr就是虛函數指針。

注意!!

注意!!

注意!!

虛函數表中存的是虛函數指針,不是虛函數。虛函數指針指向虛函數存放的地址。虛函數和普通函數一樣都是存在代碼段的。虛表也同樣存在代碼段。

虛函數小結:

1、先將基類中的虛表內容拷貝一份到派生類虛表中

2、如果派生類中重寫了基類中的某個虛函數,用派生類自己的虛函數覆蓋虛函數表中基類的虛函數

3、派生類自己新增的虛函數按其在派生類中的聲明次序將其放在派生類虛函數表中的最後

4、 虛函數表本質是一個存虛函數指針的指針數組,這個數組最後面放了一個nullptr。

多態的原理

#include <iostream>
using namespace std;
class Person{
public:
	virtual void BuyTicket() { cout << "買票-全價" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "買票-半價" << endl; }
};

void Func(Person& p) {
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

在這裏插入圖片描述

這就表明出了不同對象去完成同一操作時,是不同的形態。

也就是說想要達到多態,那麼就得必須達到兩個條件,一個是虛函數覆蓋,一個是對象的指針或者引用調用虛函數

動態綁定與靜態綁定

1.靜態綁定又稱爲前期綁定(早綁定 ),在程序編譯期間確定了程序的行爲,也稱爲靜態多態,比如:函數重載

2.動態綁定又稱後期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行爲,調用具體的函數,也稱爲動態多態。

單繼承

在單繼承中,派生類中僅有一個虛函數表,這個表和基類中的虛函數表不是同一個表,無論派生類中有沒有重寫這個虛函數。如果沒有重寫的話,基類與派生類指向的虛函數表是相同的。

在這裏插入圖片描述

如果派生類重寫了一些其他新的虛函數,那麼將會排在父類虛函數表的後面

多繼承

對於多繼承而言,這在C++中是個非常坑的地方,一般不輕易使用。只需知道,在多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中

多態常見的面試問題

  1. 什麼是多態?

    多態通俗來說就是多種形態,同一件事,但是不同的人來執行就會有不同的結果。

  2. 什麼是重載、重寫(覆蓋)、重定義(隱藏)?

  3. 多態的實現原理?

  4. inline函數可以是虛函數嗎?

    答:不能,因爲inline函數沒有地址,無法把地址放到虛函數表中。

  5. 靜態成員可以是虛函數嗎?

    答:不能,因爲靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

  6. 構造函數可以是虛函數嗎?

    答:不能,因爲對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。

  7. 析構函數可以是虛函數嗎?什麼場景下析構函數是虛函數?

    答:可以,並且最好把基類的析構函數定義 成虛函數。參考本節課件內容

  8. 對象訪問普通函數快還是虛函數更快?

    答:首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因爲構成多態,運行時調用虛函數需要到虛函數表中去查找。

  9. 虛函數表是在什麼階段生成的,存在哪的?

    答:虛函數是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。

  10. 什麼是抽象類?抽象類的作用?

    答:包含純虛函數的類叫做抽象類。抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關係。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章