菱形繼承(虛函數)->菱形虛擬繼承(虛函數)->多態系列問題

讀者注意:閱讀這篇文章時,對繼承中的對象模型要有一定了解;因爲本篇文章有的例子沒有給出類的成員變量,如果不瞭解繼承的對象模型,琢磨起來可能會很慢,覺得自己不確定的話單擊下面的“繼承”。

繼承

多態

多態按字面的意思就是“多種狀態”。在面嚮對象語言中,接口的多種不同的實現方式即爲多態。直白點理解就是不同的對象收到相同的消息時,產生不同的動作。(隨風倒)

多態可以分爲靜態多態和動態多態。
靜態多態:是在編譯器期間完成的,也叫早期綁定;
動態多態:在程序執行期間(非編譯期)判斷所引用對象的實際類型,根據其實際類型調用相應的方法,也叫動態綁定。

動態多態的構成條件:要有虛函數,並且一定要重寫,一個在基類,一個在派生類。(虛函數的函數原型、返回值、函數名、參數列表都必須一樣),必須通過基類類型的引用或者指針調用虛函數。

舉個簡單的例子:

class A
{
public:
    A(int x)//構造函數
    {
        a = x;
    }
    virtual void print()//虛函數
    {
        cout << "A::" << a << endl;
    }
private:
    int a;
};

class B :public A
{
public:
    B(int x, int y)
        :A(x)
    {
        b = y;
    }
    virtual void print()//重寫
    {
        cout << "B::" << b << endl;
    }
private:
    int b;
};

void test()
{
    A a(10);
    B b(15,20);
    A *c = &a;
    c->print();
    c = &b;
    c->print();

}

這裏寫圖片描述

有一個例外:協變;返回值類型不同(基類裏面返回基類的引用或指針,派生類裏面返回派生類的引用或指針)

重載&同名隱藏&重寫三者的區別

重載:重載只能是類的成員函數,必須在同一個作用域內(也就是同一個類裏),成員函數的名字必須相同,參數列表(個數,順序,類型)可以不同。

同名隱藏:只要成員(可以使成員變量也可以是成員函數)的名字一樣(一個在基類,一個在派生類),和參數列表、返回值沒有關係,並且要用派生類的對象來調用同名成員。

重寫:有虛函數,並且一個在基類,一個在派生類,函數的原型相同(協變除外)

哪些成員函數可以作爲虛函數,可不可採用?哪些不可以作爲虛函數?

1.構造函數(拷貝構造)
不可以,因爲有了構造函數才能構造出對象,對象中纔能有虛表;而虛函數先調用虛表,在找到虛表中的構造函數;兩者前後矛盾。

2.內聯函數
不可以,原因其實很簡單,那內聯函數就是爲了在代碼中直接展開,減少函數調用花費的代價,虛函數是爲了在繼承後對象能夠準確的執行自己的動作,這是不可能統一的。(再說了,inline函數在編譯時被展開,虛函數在運行時才能動態的邦定函數)

3.靜態成員函數
不可以,因爲靜態成員函數是全局的,對於所有的類來說,是一份公共的代碼,它不歸某個具體對象所有,所以他也沒有要動態邦定的必要性。

4.友元函數
不可以,因爲C++不支持友元函數的繼承,對於沒有繼承特性的函數沒有虛函數的說法。

5.賦值運算符重載
可以,但是不採用,雖然可以將operator=定義爲虛函數,但最好不要這麼做,使用時容易混淆。

6.析構函數
可以,而且最好將基類的析構函數聲明爲虛函數。因爲析構函數比較特殊,因爲派生類的析構函數跟基類的析構函數名稱不一樣,但是構成覆蓋,這裏編譯器做了特殊處理,減少了空間開銷。

繼承

在上一篇博客中我們提到了繼承的對象模型,這一次我們依然談到繼承的對象模型,只不過這次在類種新增了虛函數。說到虛函數,我們先剖析一下,舉個簡單的例子:

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }
    virtual void Funtest2()
    {
        cout << "A::Funtest2" << endl;
    }
    virtual void Funtest3()
    {
        cout << "A::Funtest3" << endl;
    }
    virtual void Funtest4()
    {
        cout << "A::Funtest4" << endl;
    }
};

typedef void(*PVTF)();

void PrintVpt()
{
    A a;
    PVTF* pVtf = (PVTF*)(*(int*)&a);

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{
    cout << sizeof(A) << endl;

    PrintVpt();
}

這裏寫圖片描述

也就是說在一個類裏定義了虛函數,那麼創建實例化對象之後,編譯器會創建一張虛表,對象的前四個字節存放虛表的地址,虛表裏存放各個虛函數的地址。

單繼承

第一種情況:派生類中沒有重寫虛函數,也沒有自己的虛函數,則直接繼承基類的虛函數,派生類會重新生成一張虛表,基類裏的虛表不會繼承下來。(派生類中的虛表地址和基類的虛表地址不一樣,虛表裏地址也不一樣,但是虛函數是一樣的)

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }
    virtual void Funtest2()
    {
        cout << "A::Funtest2" << endl;
    }
    virtual void Funtest3()
    {
        cout << "A::Funtest3" << endl;
    }
    virtual void Funtest4()
    {
        cout << "A::Funtest4" << endl;
    }
};

class B :public A
{

};
typedef void(*PVTF)();//函數指針

void PrintVpt()//打印虛表
{
    B b;
    PVTF* pVtf = (PVTF*)(*(int*)&b);

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;

    PrintVpt();
}

這裏寫圖片描述

這裏寫圖片描述

第二種情況:派生類中重寫了基類中的某個虛函數並且還有自己的虛函數。

如果在派生類中重寫了基類裏的虛函數,用派生類重寫後的虛函數代替基類裏的虛函數。

如果派生類定義了新的虛函數,按照聲明順序放在基類虛函數的後面。

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }
    virtual void Funtest2()
    {
        cout << "A::Funtest2" << endl;
    }
    virtual void Funtest3()
    {
        cout << "A::Funtest3" << endl;
    }
    virtual void Funtest4()
    {
        cout << "A::Funtest4" << endl;
    }
};

class B :public A
{
public:
    virtual void Funtest2()
    {
        cout << "B::Funtest2" << endl;
    }
    virtual void Funtest5()
    {
        cout << "B::Funtest5" << endl;
    }
};
typedef void(*PVTF)();//函數指針

void PrintVpt()//打印虛表
{
    B b;
    PVTF* pVtf = (PVTF*)(*(int*)&b);

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;

    PrintVpt();
}

這裏寫圖片描述

多繼承

在這裏我把所有的可能性混合到一塊:就是說派生類中對基類A、B的虛函數都進行了重寫,並且還擁有自己的虛函數。

如果派生類中對基類A、B中的虛函數進行了重寫,分別替換基類A、B中的虛函數;

如果派生類中有自己的虛函數,按照聲明順序放在第一個基類(也就是A類)虛表中虛函數的後面。

多繼承中派生類不會重新生成表,而是直接將基類A、B的虛表直接繼承下來。

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }
};

class B
{
public:
    virtual void Funtest2()
    {
        cout << "B::Funtest2" << endl;
    }
};

class C :public A, public B
{
public:

    virtual void Funtest1()
    {
        cout << "C::Funtest1" << endl;
    }
    virtual void Funtest2()
    {
        cout << "c::Funtest2" << endl;
    }
    virtual void Funtest4()
    {
        cout << "C::Funtest4" << endl;
    }
};


typedef void(*PVTF)();//函數指針

void PrintVpt()//打印虛表
{
    C c;
    PVTF* pVtf = (PVTF*)(*(int*)&c);

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
    A a;
    B &b = c;
    pVtf = (PVTF*)*(int*)&b;
    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{

    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    cout << sizeof(C) << endl;
    PrintVpt();
}

這裏寫圖片描述

這裏寫圖片描述

菱形繼承(鑽石繼承)

這裏我列舉的這種情況是派生類中有自己的虛函數,繼承時沒有重寫虛函數,這樣舉例是爲了更能直觀的看到它的對象模型。因爲一旦有重寫,在對象模型中你會分不清它到底是派生類的還是基類的。

包含虛函數的菱形繼承,派生類的對象模型其實是這樣的,本例中沒有給出類中成員變量,加上成員變量(好理解點的話就理解爲功能)的話,模型是這樣的:

這裏寫圖片描述

B1的虛表中包含了最初的基類A,以及派生類C的虛函數,B2的虛表中包含了最初的基類A的虛函數。同樣的,如果派生類中對基類B1、B2中的虛函數進行了重寫,分別替換基類B1、B2虛表中的虛函數,同理B1、B2中對A類的虛函數進行了重寫,也會替換。

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }
};

class B1:public A
{
public:
    virtual void Funtest2()
    {
        cout << "B1::Funtest2" << endl;
    }
};

class B2 :public A
{
public:

    virtual void Funtest3()
    {
        cout << "B2::Funtest3" << endl;
    }

};

class C :public B1,public B2
{
public:

    virtual void Funtest4()
    {
        cout << "C::Funtest4" << endl;
    }
};


typedef void(*PVTF)();//函數指針

void PrintVpt()//打印虛表
{
    C c;
    PVTF* pVtf = (PVTF*)(*(int*)&c);

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
    B2 &b = c;
    pVtf = (PVTF*)*(int*)&b;
    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{

    cout << sizeof(B1) << endl;
    cout << sizeof(B2) << endl;
    cout << sizeof(C) << endl;
    PrintVpt();
}

這裏寫圖片描述

虛擬繼承

單繼承(虛擬繼承)

對象模型應該是這樣的:

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }
};

class B :virtual public A
{
public:
    virtual void Funtest2()
    {
        cout << "B::Funtest2" << endl;
    }
    virtual void Funtest1()
    {
        cout << "B::Funtest1" << endl;
    }
};
typedef void(*PVTF)();//函數指針

void PrintVpt()//打印虛表
{
    B b;
    PVTF* pVtf = (PVTF*)(*(int*)&b);

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
    A &a = b;
    pVtf = (PVTF*)*((int*)&a);
    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;

    PrintVpt();
}

這裏寫圖片描述

這裏寫圖片描述

菱形虛擬繼承

class A
{
public:
    virtual void Funtest1()
    {
        cout << "A::Funtest1" << endl;
    }

    int _a;
};
class B1 :virtual public A
{
public:
    virtual void Funtest2()
    {
        cout << "B1::Funtest2" << endl;
    }

    int _b1;
};
class B2 :virtual public A
{
public:
    virtual void Funtest3()
    {
        cout << "B2::Funtest3" << endl;
    }
    virtual void Funtest1()
    {
        cout << "B2::Funtest1" << endl;
    }

    int _b2;
};
class C :public B1, public B2
{
public:
    virtual void Funtest3()
    {
        cout << "C::Funtest3" << endl;
    }
    virtual void Funtest4()
    {
        cout << "C::Funtest4" << endl;
    }

    int _c;
};

typedef void(*PVTF)();

void PrintVft()
{

    C c;//創建派生類對象d
    c._a = 1;
    c._b1 = 2;
    c._b2 = 3;
    c._c = 4;
    PVTF* pVtf= (PVTF*)*(int*)&c;

    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }

    cout << endl << endl;

    pVtf = (PVTF*)*((int*)&c + 3);
    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }

    cout << endl << endl;

    pVtf = (PVTF*)*((int*)&c + 7);
    while (*pVtf)
    {
        (*pVtf)();
        pVtf++;
    }
}
void test()
{
    cout << sizeof(C) << endl << endl;
    PrintVft();

}

這裏寫圖片描述

對象模型:

這裏寫圖片描述

在這裏總結一下:從包含虛函數的菱形虛擬繼承中我們可以看出來,它其實和不包含虛函數的菱形虛擬繼承對象結構差不了多少,只是派生類中的虛函數要寫到第一個基類裏面,在原有的偏移量表格地址上把虛表也繼承過來了。

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