C++繼承、多態與虛表


繼承

繼承的一般形式

787854ba-8e5b-4bac-a43a-86a63387ad2d

子類繼承父類,是全盤繼承,將父類所有的東西都繼承給子類,除了父類的生死,就是父類的構造和析構是不能繼承的。

繼承的訪問權限從兩方面看:

1.對象:對象只能直接訪問類中公有方法和成員。

2.繼承的子類

9f5e5c82-a834-4d8a-9aa9-40e4a708422c

私有繼承就終止了父類再往下繼承的能力

c++默認繼承為私有繼承

像以下程序

class D :public B1 ,public B2,public B3

公有繼承B1,B2,B3

class D :public B1,B2,B3;

公有繼承B1,私有繼承B2,B3

繼承是按照繼承的順序,和構造函數的初始化順序無關,看以下程序

image

如果,子類中有對象成員,構造順序是:1.父類,2.對象成員,3.自己

image

如果父類中有虛基類,應該先構造虛基類

image

虛基類

虛基類主要解決菱形繼承問題,有以下程序

image

繼承模型為:

40a072fa-e845-483a-92ab-4a526991b96e

內存模型:

9a6c638d-e400-4ce0-a814-b17531d86bbe

如果對父類的x進行賦值,如下程序,會引發錯誤,編譯器會報錯,因為繼承了兩份,會產生二義性

9216355d-2a52-416f-b675-789b6d471653

如果指定訪問A1還是A2就不會報錯

image

內存中只為A1的成員x賦值了

2f73ba47-4920-489d-b234-c2745051dab6

如果希望來自父類的x在子類中只有1份

那么就要用虛擬繼承

image

對於虛繼承來的基類,又叫做虛基類

現在對cc.c進行賦值

84f35c11-4bb7-4ede-b6fe-0dcecb051652

7f641451-63d3-4bb3-80fe-9908711512b5

A1和A2對象中的x成員都變成100

如果不是虛擬機成A1和A2繼承來的x各自是各自的空間

c43eac19-8000-46be-bbc4-7292f31704a5

虛繼承讓子類只保持父類的一份成員拷貝,A1和A2的繼承的成員的空間是一個

803575cc-85e9-431d-b4e1-b4401d4c5f2c

如果是普通繼承求C類型的大小為20字節

A1和A2各占8字節,C占4字節,加起來20字節

虛繼承時,字節為24個,理論應該是16字節,但是多了兩個虛表指針,空間就會增加

繼承中的同名隱藏

子類繼承父類,子類中有父類的同名方法,訪問的是子類的方法,子類會隱藏父類所有的同名方法,即使父類有一個同名的參數不同的方法也是如此。如下程序:

image

如果子類對象訪問父類的fun(int a)方法,編譯會報錯

09c3eb2f-d894-408c-bf7c-ac321a0292eb

但是通過作用域訪問父類方法是可以訪問的

多態

關於多態,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技 術可以讓父類的指針有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法。比如:模板技術,RTTI技術,虛函數技術,要么是試圖做到在編譯時決議,要么試圖做到運行時決議。

如果通過父類的對象調用fun,當然是調用父類的方法,因為編譯時期就決定了

image

賦值兼容規則

1.可以將子類對象賦值給父類,其實只是把子類中的父類的部分,賦值給了父類。(稱為對象的切片)

2.子類的對象賦值給父類的指針

3.子類的對象初始化父類的引用

2ae21f30-7eb8-486e-b117-1d49b96f4a44

達到多態

想要實現多態,需要動態綁定,需要父類的指針或父類的引用

父類方法為虛方法,子類覆蓋父類的虛方法,才能達到多態

上述程序中的指針和引用調用

pb->fun();
rb.fun();

就會訪問到子類的方法

子類中父類沒有的方法,父類指針也無法訪問到,父類指針只能訪問到父類自己有的范圍

注意:

virtual與函數重載無關,基類沒有virtual,子類的同名方法就是隱藏而不是覆蓋

只要沒有指針,父類調用子類的,全部是調用父類作用范圍內的,就算子類覆蓋了父類的virtual方法也是如此。

子類覆蓋了父類的虛方法,子類的方法也是虛方法,即使前面不寫不寫virtual也是如此

子類要覆蓋父類的方法,就是要函數名參數都必須一樣才叫覆蓋

抽象類

如果基類是一個抽象的概念,就可以為其定義純虛函數構成一個抽象類

以下的抽象類就定義了三個純虛函數

image

注意:

如果一個類具有一個或者多個純虛函數,那么它就是一個抽象類,必須讓其他類繼承它。

抽象類位於類層次的上層,不能定義抽象類的對象。

基類中的純虛函數沒有代碼,而且必須在子類中覆蓋基類中的純虛函數。

子類中如果沒有實現其抽象父類的所有純虛函數,其也是一個抽象類,也不能實例化對象。

抽象類和虛函數的類有區別:

抽象類中的純虛函數不實現,並且繼承他的子類必須實現它

虛函數基類中可以有實現,並且子類中如果重新覆蓋了它,還可以實現多態機制

多態的實現

先看一個程序

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

class D : public Base
{
public:
    void fun()
    {
        cout<<"This is D::fun()"<<endl;
    }
};

void main()
{
    D d;
    d.fun();
    d.fun(0);
}

當父類沒有virtual時,d對象的內存模型

image

當父類有了virtual時,d對象的內存模型

3f74c113-9c30-4e43-aead-c1fed2c5be92

可以看到,父類會多出來一個虛表指針,保存一個子類的方法

再來看一個程序

class Base
{
public:
    Base():y(0)
    {
        cout<<"Create Base Object."<<endl;
    }
public:
    virtual void fun()
    {
        cout<<"This is Base::fun()"<<endl;
    }
    virtual void list()
    {
        cout<<"This is Base::list()"<<endl;
    }

    void print()
    {
        cout<<"This is Base::print()"<<endl;
    }
public:
    int y;
};

class D : public Base
{
public:
    D():x(0)
    {
        cout<<"Create D Object."<<endl;
    }
public:
    void fun()
    {
        cout<<"This is D::fun()"<<endl;
    }
    void list()
    {
        cout<<"This is D::list()"<<endl;
    }

    void print()
    {
        cout<<"This is D::print()"<<endl;
    }
    
private:
    int x;
};

void main()
{
    D d;
    Base *pb = &d;
    pb->fun();
    pb->list();
    pb->show();
}

因為創建子類對象前,要先構造父類對象,其內存模型為

3ce9950d-a1e8-47bd-972b-88eae9fa2a1b

虛表中存儲的都是父類的虛函數指針

只要有虛方法,創建對象就會自動建立一個虛表,表的最后一個位置為NULL

26c4fa66-7357-431c-83b7-bbd4317eab65

后面的八個0就是表的最后一個位置NULL (不同平台可能不一樣,這是VC平台下的結果)

虛表中存儲父類的方法的地址,虛表指針指向這塊地址,這就是為什么加了virtual后多了4個指針的原因

3598f77a-d261-4951-8837-7da32031c7f8

子類對象地址為什么能賦值給父類對象指針?

因為,子類對象地址賦值給父類對象指針,父類對象指針就指向了子類的對象空間,父類操作子類的范圍是有限制的,只能操作到子類中父類的范圍。

上面談到構造子類對象前先構造一個父類對象

當構造父類對象完成時候,再構造子類時,可以看到,虛表中的方法已經被更改為子類的了

3107da1c-b65f-40d5-8d00-87ea6ebdd0a1

現在我們在父類中加一個虛的show()方法,而子類中不去覆蓋父類的show方法,可以看到子類中虛表的成員show還是父類的函數指針

5e6e118b-8504-4a89-b116-ba44de106773

用父類的指針來調用,是在虛表中調用,但是經過子類的覆蓋,虛表中的函數地址已經變成了子類的函數的地址了,所以會調用子類的方法

如下圖

59c1e61b-ce45-498f-a4cc-739ba40a441c

通過指針訪問虛表的函數成員

有以下程序

class Base 
{ 
public: 
    virtual void f()
    { cout << "Base::f"<<endl;}
    virtual void g()
    { cout << "Base::g"<<endl;}
    virtual void h()
    { cout << "Base::h"<<endl;}
private:
    int a;
    int x;
}; 

typedef void(*pFun)();

void main()
{
    Base b;
    cout<<"&b = "<<&b<<endl;
    cout<<"vfptr = "<<hex<<*(int*)(&b)<<endl;    //虛表的地址,前四個字節

    pFun pfun = (pFun)*(((int*)*(int*)(&b))+0);    //取虛表中第一個虛函數的地址
    pfun();
    pfun = (pFun)*(((int*)*(int*)(&b))+1);
    pfun();
    pfun = (pFun)*(((int*)*(int*)(&b))+2);
    pfun();
}

注意:虛表是在一個對象空間的開始位置

通過強行把&b轉成int *,取得虛函數表的地址,然后,再次取址就可以得到第一個虛函數的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int* 強制轉成了函數指針)。

畫個圖解釋一下。如下所示:

0.92727677412201

注意:在上面這個圖中,我在虛函數表的最后多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符“/0”一樣,其標志了虛函數表的結束。這個結束標志的值在不同的編譯器下是不同的。

在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最后一個虛函數表。(上面內容提到過)

下面,我將分別說明“無覆蓋”和“有覆蓋”時的子類虛函數表的樣子。沒有覆蓋父類的虛函數是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

一般繼承(無虛函數覆蓋)

下面,再讓我們來看看繼承時的虛函數表是什么樣的。假設有如下所示的一個繼承關系:

0.12154239708264525

請注意,在這個繼承關系中,子類沒有重寫任何父類的函數。那么,在派生類的實例的虛函數表如下所示:

對於實例:Derive d; 的虛函數表如下:(overload(重載) 和 override(重寫),重載就是所謂的名同而簽名不同,重寫就是對子類對虛函數的重新實現。)

0.8101163589599465

我們可以看到下面幾點:

1)虛函數按照其聲明順序放於表中。

2)父類的虛函數在子類的虛函數前面。

一般繼承(有虛函數覆蓋)

覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。

0.2945619967221582

為了讓大家看到被繼承過后的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那么,對於派生類的實例的虛函數表會是下面的樣子:

0.0692499251192753

我們從表中可以看到下面幾點,

1)覆蓋的f()函數被放到了子類虛函數表中原來父類虛函數的位置。

2)沒有被覆蓋的函數依舊。

這樣,我們就可以看到對於下面這樣的程序,

Base *b = new Derive();

b->f();

由b所指的內存中的虛函數表(子類的虛函數表)的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。

多重繼承(無虛函數覆蓋)

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。注意:子類並沒有覆蓋父類的函數

0.8060241828430086

對於子類實例中的虛函數表,是下面這個樣子:

0.8257025783841201

我們可以看到:

1) 每個父類都有自己的虛表。

2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

多重繼承(有虛函數覆蓋)

下面我們再來看看,如果發生虛函數覆蓋的情況。

下圖中,我們在子類中覆蓋了父類的f()函數。

0.5885229699955636

下面是對於子類實例中的虛函數表的圖:

0.006813835416641378

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以用任一個父類指針來指向子類,並調用子類的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

安全性

一、嘗試:通過父類型的指針(指向子類對象)訪問子類自己的虛函數

我們知道,子類沒有重載父類的虛函數是一件毫無意義的事情。因為多態也是要基於函數重載的。雖然在上面的圖中我們可以看到子類的虛表中有Derive自己的虛函數,但我們根本不可能使用基類的指針來調用子類的自有虛函數:

Base1 *b1 = new Derive();
b1->f1(); //編譯出錯

任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行為都會被編譯器視為非法,所以,這樣的程序根本無法編譯通過。

但在運行時,我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行為。

二、嘗試:通過父類型的指針(指向子類對象)訪問父類的non-public虛函數

另外,如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在於子類虛函數表中,所以我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的。

如下程序:

class Base {
private:
    virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};

typedef void(*Fun)(void);

void main() {
    Derive d;
    Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
    pFun();
}

 

[參考:https://blog.csdn.net/sanfengshou/article/details/4574604]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM