1. 程式人生 > >詳細瞭解C++多型

詳細瞭解C++多型

什麼是多型??

1.多型性(polymorphism)據說最早來自希臘語,從字面上理解就是多種形態,多種形式。具體到C++這種面向物件的語言(OOP)中,其實就是一種“介面”,多種實現(方法)

2.多型可以分為靜多型和動多型,具體的分類如下

這裡寫圖片描述

3.靜多型和動多型的區別:

其實只是區別什麼時間將函式實現和函式呼叫關聯起來,是在編譯期還是在執行期,即函式地址是在早繫結還是晚繫結的??

①靜多型是指在編譯期就可以確定函式的呼叫地址,併產生程式碼,這就是靜態的,也就是說地址是早早繫結的,靜多型往往也被叫做靜態聯翩。

②動多型則是指函式呼叫的地址不能在編譯期間確定的,必須要在執行時才能確定,這就屬於晚繫結,動多型往往也被叫做動態聯翩。

4.具體實現

靜多型往往是通過函式過載和模板(泛型程式設計)來實現,具體 見一下程式碼:

//兩個函式構成過載的靜多型

int add(int a,int b)
{
    return a + b;
}

double add(double a,double b)
{
    return a + b;
}

//構成函式過載的條件:相同函式名
//函式模板(泛型程式設計)
template <typename T>
//temolate<class T>
T add(T a,T b)
{
    return a + b;
}

多型存在的意義

C++中的幾大特性:繼承,封裝,多型。封裝可以是的程式碼模組化,繼承可以使在原有的程式碼基礎上擴充套件,他們的目的都是為了能夠使程式碼複用。而多型是為了介面重用。也就是說不論傳遞過來的究竟是哪個類的物件(主要是:基類和派生類),函式都能通過同一個介面呼叫到適合各自物件的實現方法。

在具體講解多型的前提下我們先來了解下面的知識點:

class Base{
public:
    void fun()
    {
        cout<<"Base::fun()"<<endl;
    }
};//基類

class Derived : public Base{

    public:
        //void fun()
        //{
        //    cout<<"Derived::fun()"<<endl;
        //)
};

void FunTest()
{
    Base b;
    Derived d;
    b.fun();//呼叫基類的fun列印Base::fun()
    d.fun();//子類雖然繼承了父類但是父類中沒有成員方法。列印Base::fun()
}
int main()
{
    FunTest();
    return 0;
}

但是當我們放開了子類中fun函式的註釋:

我們會發現基類物件依然呼叫Base::fun(),子類雖然繼承了基類的fun但是子類本身中的fun構成了(重定義)隱藏,即基類中的fun被子類中的隱藏此時 我們子類中列印的是Derived::fun。

如我們想呼叫基類中被隱藏的成員函式:加上作用域d.Base::fun(),此時列印Base::fun()

依舊保持上面的類定義不變,我們來試試用指標來呼叫:

void FunTest()
{
    Base b;
    Derived d;
    Base* pb = &b;
    Derived* pd = &d;
    pb->fun();//pb指向基類列印Base::fun()
    pd->fun();//pd指向子類,列印Derived::fun()

    pb = &d;
    pb->fun();//基類指標指向子類物件,卻列印Base::fun()

//同樣得道理,對於引用
    Base& rb = b;
    Derived& rd = d;
    rb.fun();//rb引用基類,列印Base::fun()
    rd.fun();//rd引用子類,列印Derived::fun()

    Base& rd2 = d;//基類引用rd2引用子類,列印Base::fun()
    rd2.fun();
}
int main()
{
    FunTest();
    return 0;
}

我們知道C++繼承中有複製相容,即基類指標可以指向子類物件。那麼為什麼還會出現基類指標指向子類或者基類物件引用子類物件,卻呼叫基類自己的fun列印函式?這就是我們上面講的靜態聯翩導致的:在編譯期就將函式實現和函式呼叫關聯起來,不管是引用還是指標在編譯期都是Base型別的所以其自然呼叫Base類的fun函式。

那麼我們如何來實現讓他呼叫子類中的fun函式呢??我們引入了動多型

所謂的動多型就是通過 繼承+虛擬函式 來實現的,只有程式執行期間(非編譯期間)才能判斷所引用物件的實際型別,,根據其實際型別呼叫相應的方法。具體格式就是使用virtual關鍵字修飾類的成員函式時,指明該函式為虛擬函式,並且派生類需要重新實現該成員方法,編譯器實現動態繫結。

在上面的程式碼如果我們在基類的fun()函式前加上virtual即可實現動態繫結:

class Base{
   public:
    virtual void fun()
    {
        cout<<"Base::fun()"<<endl;
    }
};

這樣我們的子類指標呼叫的函式就是子類中的,基類指標指向子類物件也是呼叫子類中的成員函式

需要注意的是:【動態繫結的條件】

1.必須是虛擬函式(實現函式覆蓋)

2.必須通過基類型別的指標或者引用來呼叫該虛擬函式(通過查詢虛表,如果通過物件呼叫,直接就會在該物件的成員方法中尋找)

知識補充:

我們知道C++中虛擬函式允許子類重新定義成員函式,而子類重新定義父類的做法稱為覆蓋(override),或者稱為重寫。我們首先要區分幾個概念:過載,重寫(覆蓋),以及重定義(同名隱藏)。

所謂過載是指在同一作用域中允許有多個同名函式,而這些函式的引數列表不同,包括引數的個數不一樣,引數型別不同,引數的次序不同,需要注意的是返回值相同與否並不影響是否過載(返回值型別)。

而重寫(覆蓋)和重定義(同名隱藏)有點像,區別是在寫重寫的函式是否是虛擬函式,只有重寫了 虛擬函式的才能算是體現了C++的多型性,否則為重定義

這裡寫圖片描述

純虛擬函式

在成員函式的形參後面加上= 0;則成員函式為純虛擬函式。包含純虛擬函式類叫做抽象類(也叫做介面類),抽象類不能例項化出物件。純虛擬函式必須在派生類中重新定義後,派生類才能例項化出物件

class Person
{
    public:
        virtual void display() = 0;
    private:
        string _name;
};

class Student:public Person
{
    void Display()
    {
        cout<<"Student"<<endl;
    }
};

int main()
{
    //Person p;//不能用來例項話物件
    Student stu;
}

 抽象類往往用於以下情況,它可以方便我們使用多型特性,且很多情況下,基類本身生成物件是不合情理的,我們知道所有的物件都是通過類來描繪的,但是反過來不成立:並不是所有的類都是用來描繪物件的,如果一個類中沒有包含足夠的資訊來描繪一個具體的物件,這樣的類就要使用抽象類,就像一個水果類可以派生出橘子,香蕉蘋果等等,但是水果本身定義物件並不合理也沒有必有。

要點

1.派生類重寫基類的虛擬函式實現多型,要求函式名,引數列表,返回值完全相同。(協變除外)

2.基類中定義虛擬函式,派生類中該函式始終保持虛擬函式的特性。

3.只有類的非靜態成員函式才能定義為虛擬函式,靜態成員函式和友元函式不能定義為虛擬函式。

4.如果在類外定義虛擬函式,只有在宣告函式時加上virtual關鍵字,定義時不用加。

5.建構函式不能定義為虛擬函式,雖然可以將operator=定義為虛擬函式 ,但最好不要這麼做,使用時容易混淆。

6.不要在建構函式和解構函式中呼叫虛擬函式,在建構函式和解構函式中,物件是不完整的,會出現未定義的行為。

7.最好將基類的解構函式宣告為虛擬函式。(解構函式比較特殊,因為派生類的解構函式和基類的解構函式名稱不一樣,但是構成覆蓋,這裡編譯器做了特殊處理)。

8.虛表是所有類物件例項公用的。

我們來看下面這組程式碼:

class A
{
public:
    virtual void Display()
    {}
};

class B
{
public:
    void Display()
    {}
};

int main()
{
    cout<<sizeof(A)<<endl;//4
    cout<<sizeof(B)<<endl;//1
    getchar();
}

 我們知道空類佔一個位元組(為什麼呢?),那為什麼加了virtual關鍵字就變成了4?

這是因為每個有虛擬函式的類或者虛繼承的子類,編譯器都會為他們生成一個虛擬函式表(簡稱:虛表)表中的每個元素都會指向一個虛擬函式的地址。(注意虛表是從屬的)

此外,編譯器會為每個包含虛擬函式的類加上一個成員變數,是一個指向該虛擬函式表的指標( vptr),每一個由此類別派生出來的類,都有這麼一個vptr。虛表指標都是從屬物件的,也就是說該類如果含有虛表,則該類的所有物件都會含有一個虛表指標,並且該虛表指標指向同一個虛表,因此這裡的4是指標的大小。