首頁 > 軟體

C++ 超全面講解多型

2022-04-15 13:01:15

多型的概念

概念:通俗的來說就是多種形態,具體就是去完成某個行為,當不同型別的物件去完成同一件事時,產生的動作是不一樣的,結果也是不一樣的。

舉一個現實中的例子:買票這個行為,當普通人買票時是全價;學生是半價;軍人是不需要排隊。

多型也分為兩種:

  • 靜態的多型:函數呼叫
  • 動態的多型:父類別指標或參照呼叫重寫虛擬函式。

這裡的靜態是指在編譯時實現多型的,而動態是在執行時完成的。

多型的定義及實現

構成條件

多型一定是建立在繼承上的,那麼除了繼承還要兩個條件:

  • 必須通過基礎類別(父類別)的指標或參照呼叫函數
  • 被呼叫的函數必須是虛擬函式,且派生類(子類)必須對積累的虛擬函式進行重寫。

虛擬函式

概念:被virtual修飾的類成員函數稱為虛擬函式

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

注意:

  • 只有類的非靜態成員函數可以是虛擬函式
  • 虛擬函式這裡virtual和虛繼承中用的是同一個關鍵字,但是他們之間沒有關係;虛擬函式這裡是為了實現多型;虛繼承是為了解決菱形繼承的資料冗餘和二義性,它們沒有關聯

虛擬函式的重寫

概念:派生類(子類)中有一個跟基礎類別(父類別)完全相同的虛擬函式(即派生類虛擬函式與基礎類別虛擬函式的返回值型別,函數名字,參數列完全相同),稱子類的虛擬函式重寫了基礎類別的虛擬函式。

例:

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"全價票"<<endl;
    }
};
​
class Student :public Person
{
public:
    //子類的虛擬函式重寫了父類別的虛擬函式
    virtual void BuyTicket()
    {
        cout<<"半價票"<<endl;
    }
};
​
class Soldier : public Person
{
public:
    //子類的虛擬函式重寫了父類別的虛擬函式
    virtual void BuyTicket()
    {
        cout<<"優先買票"<<endl;
    }
};
//多型的實現
void f(Person& p)//這塊的引數必須是參照或者指標
{
    p.BuyTicket();
}
​
int main()
{
    Person p;
    Student st;
    Soldier so;
    
    f(p);
    f(st);
    f(so);
    
    return 0;
}

注意:這裡子函數的虛擬函式可以不加virtual,也算完成了重寫,但是父類別的虛擬函式必須要加,因為子類是先繼承父類別的虛擬函式,繼承下來後就有了virtual屬性了,子類只是重寫這個virtual函數;除了這個原因之外,還有一個原因,如果父類別的解構函式加了virtual,子類加不加都一定完成了重寫,就保證了delete時一定能實現多型的正確呼叫解構函式。

虛擬函式重寫的兩個例外

1、協變

概念:派生類重寫基礎類別虛擬函式時,與基礎類別虛擬函式返回值型別不同。即基礎類別虛擬函式返回基礎類別物件的指標或者參照,派生類虛擬函式返回派生類物件的指標或者參照時,稱為協變

例:

class A{};
class B : public A{};
​
class Person
{
public:
    virtual A* f()
    {
        return new A;
    }
};
​
class Student : public Person
{
public:
    virtual B* f()           //返回值不同但是構成虛擬函式重寫
    {
        return new B;
    }
};

2、解構函式的重寫

如果基礎類別的解構函式為虛擬函式,此時派生類解構函式只要定義,無論是否加virtual關鍵字,都與基礎類別的解構函式構成重寫,雖然基礎類別與派生類解構函式名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這裡可以理解為編譯器對解構函式的名稱做了特殊處理,編譯後解構函式的名稱統一處理成destructor

例:

class Person {
public:
    //建議把父類別解構函式定義為虛擬函式,這樣方便子類的虛擬函式重寫父類別的虛擬函式
    virtual ~Person() {cout << "~Person()" << endl;}
};
​
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生類Student的解構函式重寫了Person的解構函式,下面的delete物件呼叫解構函式,才能構成多型,才能保證p1和p2指向的物件正確的呼叫解構函式。
int main()
{
    Person* p1 = new Person;
   //這裡p2指向的子類物件,應該呼叫子類解構函式,如果沒有呼叫的話,就可能記憶體漏失
    Person* p2 = new Student;
    //多型行為
    delete p1;
    delete p2;
    //只有解構函式重寫了那麼這裡delete父類別指標呼叫解構函式才能實現多型。
    return 0;
}

C++11 override和finel

從上面可以看出,C++對函數重寫的要求比較嚴格,但是有些情況下由於疏忽,可能會導致函數名字母次序寫反而無法構成過載,而這種錯誤在編譯期間是不會報出的,只有在程式執行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助使用者檢測是否重寫

final:修飾虛擬函式,表示該虛擬函式不能再被重寫

class Car
{
public:
    virtual void Drive() final {}
};
class Benz :public Car
{
public:
    //會在這塊報錯,因為基礎類別的虛擬函式已經被final修飾,不能被重寫了
    virtual void Drive() {cout << "Benz-舒適" << endl;}
};  

override: 檢查派生類虛擬函式是否重寫了基礎類別某個虛擬函式,如果沒有重寫編譯報錯

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

過載、覆蓋(重寫)、隱藏(重定義)的對比

抽象類

抽象類的概念

純虛擬函式:在虛擬函式的後面加上=0就是純虛擬函式,有純虛擬函式的類就是抽象類,也叫介面類,抽象類無法範例化物件。抽象類的子類不重寫父類別的虛擬函式的話,也是一個抽象類。

//抽象類的定義
class Car
{
public:
    virtual void run()=0;   //不用實現只寫介面就行。   
};

純虛擬函式不寫函數體,並不意味著不能實現,只是我們不寫。因為寫出來也沒有人用。

虛擬函式的作用

  • 強制子類重寫虛擬函式,完成多型。
  • 表示抽象類。

介面繼承和實現繼承

普通函數的繼承就是實現繼承,虛擬函式的繼承就是介面繼承。子類繼承了函數的實現,可以直接使用。虛擬函式重寫後只會繼承介面,重寫實現。所以如果不用多型,就不要把函數寫為虛擬函式。

純虛擬函式就體現了介面函數。下面我們來實現一道題,展現一下介面繼承。

class A
{
public:
    virtual void fun(int val=0) 
    {
        cout<<"A->val = "<<val <<endl;
    }
    void Fun()
    {
        fun();
    }
};
​
class B:public A
{
public:
    virtual void fun(int val=1)
    {
        cout<<"B->val"<<val<<endl;
    }
};
​
int main()
{
    B b;
    A* a=&b;
    a->Fun();
    return 0;
}

結果列印為 :B->val=0

子類物件切片給父類別指標,傳給Fun函數,滿足多型,會去呼叫子類的fun函數,但是子類的虛擬函式繼承了父類別的介面,所以val是父類別的0。

多型的原理

虛擬函式表

class A
{
public:
    virtual void fun()
    {
        
    }
    protected:
    int _a;
};

sizeof(A)是多少?

列印出來是8。

我們定義了一個A型別的物件a,開啟偵錯視窗,發現a的內容如下

我們發現出了成員變數_a以外,還多了一個指標,這個指標是不準確的,實際上應該是 _vftptr(virtual function table pointer),虛擬函式表指標。在計算類大小的時候要加上這個指標的大小。虛表就是存放虛擬函式的地址地方,當我們去呼叫虛擬函式,編譯器就會通過虛表指標去虛表裡查詢。

class A
{
public:
    void fun1()
    {
        
    }
    virtual void fun2()
    {}
};
​
int main()
{
    A* a=nullptr;
    a->fun1();//呼叫函數,因為這是普通函數的呼叫
    a->fun2();//呼叫失敗,虛擬函式需要對指標操作,無法操作空指標。
    return 0;
}

實現一個繼承

class A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
class B : public A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
​
int main()
{
    A a;
    B b;
    return 0;
}

子類與父類別一樣有一個虛表指標。

子類的虛擬函式表一部分繼承自父類別。如果重寫了虛擬函式,那麼子類的虛擬函式會在虛表上覆蓋父類別的虛擬函式。

本質上虛擬函式表是一個虛擬函式指標陣列,最後一個元素是nullptr,代表虛表的結束。所以,如果繼承了虛擬函式,那麼

  • 子類先拷貝一份父類別虛表,然後用一個虛表指標指向這個虛表。
  • 如果有虛擬函式重寫,那麼在子類的虛表上用子類的虛擬函式覆蓋。
  • 子類新增的虛擬函式按其在子類中的宣告次序增加到子類虛表的最後。

虛擬函式表放在記憶體的那個區,虛擬函式又放在哪?

虛擬函式與虛擬函式表都放在程式碼段。

多型的原理

我們現在來看多型的原理

class person
{
public:
    virtual void fun()
    {
        cout<<"全價票"<<endl;
    }
};
class student : public person
{
public:
    virtual void fun()
    {
        cout<<"半價票"<<endl;
    }
};
void buyticket(person* p)
{
    p->fun();
}

這樣就實現了不同物件去呼叫同一函數,展現出不同的形態。 滿足多型的函數呼叫是程式執行是去物件的虛表查詢的,而虛表是在編譯時確定的。 普通函數的呼叫是編譯時就確定的。

動態繫結與靜態繫結

1.靜態繫結又稱為前期繫結(早繫結),在程式編譯期間確定了程式的行為,也稱為靜態多型,比如:函數過載

2.動態繫結又稱後期繫結(晚繫結),是在程式執行期間,根據具體拿到的型別確定程式的具體行為,呼叫具體的函數,也稱為動態多型。我們說的多型一般是指動態多型。

這裡我附上一個有意思的問題:

就是在子類已經覆蓋了父類別的虛擬函式的情況下,為什麼子類還是可以呼叫“被覆蓋”的父類別的虛擬函式呢?

#include <iostream>
using namespace std;
​
class Base {
public:
    virtual void func() {
        cout << "Base funcn";
    }
};
​
class Son : public Base {
public:
    void func() {
        Base::func();
        cout << "Son funcn";
    }
};
​
int main()
{
    Son b;
    b.func();
    return 0;
}

輸出:

Base func

Son func

這是C++提供的一個迴避虛擬函式的機制

通過加作用域(正如你所嘗試的),使得函數在編譯時就係結。

到此這篇關於C++ 超全面講解多型的文章就介紹到這了,更多相關C++ 多型內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com