C++總結5——繼承與多態

一、繼承
1.C++的繼承
繼承有3種形式:私有繼承、保護繼承、公有繼承,缺省的繼承方式是私有繼承。

不論哪種繼承方式,派生類都是顯示的繼承類基的保護成員變量和函數和公有成員變量和函數,繼承方式只是限定在派生類中這兩種成員變量的訪問方式(即訪問權限)。私有的成員變量和函數也被繼承到派生類中,但是不能被訪問,它是隱藏的,在派生類中不可見
派生類繼承基類,除基類的構造函數和析構函數不外,其他的所有都繼承。

繼承是“is-a”的關係。派生類是基類的一種,例如:學生繼承與人,學生是人的一種,但人不一定都是學生。

私有繼承:將基類保護和公有的成員變量作爲派生類的私有變量,不能在類外訪問;
保護繼承:將基類保護和公有的成員變量作爲派生類的保護變量,不能在類外訪問但是可以被繼承;
公有繼承:將基類保護和公有的成員變量作爲派生類的公有變量,可以在類外訪問。

注意:派生類中可以定義和基類同名的成員變量,因爲有類的作用域的限定(Base::a Derive::a)

class Base
{
public:
    Base(int date):_ma(data)   {}
    ~Base()  {}
private:
    int _ma;
};

class Derive : public Base
{
public:
    Derive(int data1, int data2):Base(data1),_mb(data2)  { }
    ~Derive()  {};
};

在派生類的構造函數的初始化成員列表中顯示的調用基類的構造函數,用基類的構造函數構造派生類中的基類部分。

2.派生類對象的構造/析構順序
構造:構造基類成員對象–>構造基類–>構造派生類成員對象–>構造派生類
析構:析構派生類–>析構派生類的成員對象–>析構基類–>析構基類的成員對象

3.基類對象==>賦值給==>派生類對象 (X) 後面派生類的內存無法賦值
派生類對象==>賦值給==>基類對象 (V)將派生類中基類的部分賦值給基類對象
基類指針==>指向==>派生類對象 (V) 該指針智能訪問從基類繼承的那部分
派生類指針==>指向==>基類對象 (X) 可能非法訪問內存

4.虛函數
如果派生類繼承了有被vritual關鍵字修飾的函數的基類,被vritual修飾的函數稱爲虛函數。派生類可以重寫該虛函數。如果派生類重寫了該虛函數,那麼派生類對象調用該方法時調用的就是派生類自己實現的方法。如果派生類沒有重寫該方法,則調用基類的方法。

class Base
{
public:
    Base(int data):_ma(data)   {}
    ~Base()  {}
    virtual void Show()//虛函數
    {
        cout<<"Base::Show()"<<endl;
    }
private:
    int _ma;
};

class Derive : public Base
{
public:
    Derive(int data1, int data2):Base(data1),_mb(data2)  { }
    ~Derive()  {};
    virtual void Show()//虛函數
    {
        cout<<"Derive::Show()"<<endl;
    }
private:
    int _mb;
};

int main()
{
    Derive derive(10,20);
    derive.Show();//Derive::Show()
    return 0;
}

5.純虛函數

純虛函數是特殊的虛函數,基類中不能給出這個虛函數的實現方法,派生類必須給出該函數的實現。這種特殊的函數稱爲純虛函數,有純虛函數的類稱爲抽象類,抽象類不能實例化對象,但是可以定義抽象類的指針或引用,派生類必須重寫方法後才能實例化對象。

class Base//抽象類
{
public:
    Base(int date):_ma(data)   {}
    ~Base()     {}
    vritual void Show() = 0//純虛函數
private:
    int _ma;
};

含有虛函數的類,編譯器會在其前4個字節添加一個虛表指針,併爲其產生一個虛函數表,虛表指針指向虛函數表。虛函數表在編譯時產生,位於.rodata段,屬於類所共有,不屬於某一對象。若派生類沒有從基類中繼承虛虛表指針和函數表,則自己產生,虛表指針永遠位於對象前4個字節處。如果派生類從基類繼承了虛表指針和虛函數表,則派生類不再產生,虛函數都寫在繼承來的虛函數表中(同名覆蓋)。

6.基類的成員方法和派生類的成員方法之間是同名隱藏的關係
基類和派生類繼承結構中,函數名、參數列表、返回值都相同,如果基類中的函數是vritual函數,那麼派生類中該函數也是vritual函數。如果派生類重新實現了該vritual函數,那麼派生類對象調用該方法時調用的就是派生類自己實現的方法。如果派生類沒有重寫該方法,則調用基類的方法。

7.菱形繼承
兩個派生類繼承同一個基類,而某一個基類同時繼承這兩個派生類。

class A
{
public:
    A(int data):ma(data)
    {
        cout<<"A()"<<endl;
    }
    ~A()
    {
        cout<<"~A()"<<endl;
    }
private:
    int ma;
};

class B : public A
{
public:
    B(int data):A(data),mb(data)
    {
        cout<<"B()"<<endl;
    }
    ~B()
    {
        cout<<"~B()"<<endl;
    }
private:
    int mb;
};

class C : public A
{
public:
    C(int data):A(data),mc(data)
    {
        cout<<"C()"<<endl;
    }
    ~C()
    {
        cout<<"~C()"<<endl;
    }
private:
    int mc;
};

class D : public B, public C
{
public:
    D(int data):B(data),C(data),md(data)
    {
        cout<<"D()"<<endl;
    }
    ~D()
    {
        cout<<"~D()"<<endl;
    }
private:
    int md;
};

int main()
{
    D d(10);
    return 0;
}

通過命令 cl 文件名 /d1reportSingleClassLayout類名 > 重定向文件名
例:cl 20170717.cpp /d1reportSingleClassLayoutD > log.txt
將20170717.cpp文件中,類D的內存佈局重定向到文件log.txt中

類D的內存佈局:
這裏寫圖片描述
d中有兩個ma,但是變量ma所處的類的作用域不同。

8.虛繼承

虛繼承是多重繼承中特有的概念,是爲了解決多重繼承而出現的。但是在C++中,多重繼承是不推薦的,並不常用。被虛繼承的基類叫做虛基類。

class A
//虛基類
{
public:
    A(int data):ma(data)
    {
        cout<<"A()"<<endl;
    }
    ~A()
    {
        cout<<"~A()"<<endl;
    }
private:
    int ma;
};

class B : vritual public A
{
public:
    B(int data):A(data),mb(data)
    {
        cout<<"B()"<<endl;
    }
    ~B()
    {
        cout<<"~B()"<<endl;
    }
private:
    int mb;
};

class C : vritual public A
{
public:
    C(int data):A(data),mc(data)
    {
        cout<<"C()"<<endl;
    }
    ~C()
    {
        cout<<"~C()"<<endl;
    }
private:
    int mc;
};

class D : public B, public C
{
public:
    //虛繼承後,ma只有一份。類D中,A可見,因此要在初始化類表中對其進行構造。
    //虛基類的數據永遠先構造
    D(int data):B(data),C(data),A(data),md(data)
    {
        cout<<"D()"<<endl;
    }
    ~D()
    {
        cout<<"~D()"<<endl;
    }
private:
    int md;
};

int main()
{
    D d(10);//構造順序:A(); B(); C(); D();
    return 0;
}

類D的內存佈局:
這裏寫圖片描述

虛繼承中,類的內存佈局:將虛基類中的數據放在派生類數據的最後面,在原虛基類的部分添加vbptr(虛表指針)

二、多態
1.多態的實現機制
C++的多態就是基於繼承的,多態的實現就是調用虛函數時發生的同名覆蓋。當用基類的指針(或引用)指向派生類的對象時,通過該指針(或引用)調用虛方法是動態聯編的過程。先找到對象前4個字節的虛函數指針(vbptr),通過vbptr找到虛函數表,虛函數表裏有函數的入口地址。

2.C++的靜多態和動態多態
靜多態是指函數的重載和模板
動多態是指繼承中,虛函數的同名覆蓋方法

3.早綁定和晚綁定的區別
早綁定也稱靜態綁定,是程序在編譯時就確定調用的是哪個函數。
彙編指令是 call Base::func()

晚綁定也稱動態綁定,是編譯的時候才確定調用的是哪個函數。晚綁定基於繼承實現,基類的指針(或引用)指向派生類的對象,通過指針(或聽引用)訪問虛函數時,會調用指針所指向的派生類的方法。
彙編指令如下:
mov ecx,dword ptr[p] 訪問虛表指針,將虛表指針放在ecx寄存器中
mov eax,dword ptr[ecx] 將ecx(虛表指針)的值(虛函數表)放在eax寄存器中
call eax 調用函數
在運行過程中,確定了eax寄存器裏的值,才能確定調用哪個函數。

4.如果基類的指針(或引用)指向堆上派生類的對象,必須要將基類的構造函數寫成虛函數,否則會有內存泄露。

#include <iostream>
using namespace std;

class Base
{
public:
    Base(int data):_ma(data)  
    {
        cout<<"Base()"<<endl;
    }
    ~Base()  
    {
        cout<<"~Base()"<<endl;
    }
    virtual void Show()//虛函數
    {
        cout<<"Base::Show()"<<endl;
    }
private:
    int _ma;
};

class Derive : public Base
{
public:
    Derive(int data1, int data2):Base(data1),_mb(data2)  
    {
        cout<<"Derive()"<<endl;
    }
    ~Derive()  
    {
        cout<<"~Derive()"<<endl;
    };
    virtual void Show()//虛函數
    {
        cout<<"Derive::Show()"<<endl;
    }
private:
    int _mb;
};

int main()
{
    Base *p = new Derive(10,20);
    p->Show();
    delete p;

    return 0;
}

運行結果如下圖:
這裏寫圖片描述
只調用了基類的析構函數,Derive類未析構。原因是:p是Base*類型的直指針,Base的析構函數不是虛函數,編譯時已經確定delete p時調用的是Base類的析構函數。
這裏寫圖片描述

如果將基類的析構函數寫成虛析構函數,則調用delete p時,具體調用哪個函數,是運行時才確定的。當程序運行時,發現p所指向的是派生類的對象,故先調用派生類的析構函數,然後調用基類的析構函數。
這裏寫圖片描述
這裏寫圖片描述

一般都將基類的虛構函數定義爲虛函數,從而避免內存泄露。

5.常見的面試題
<1>inline函數能否爲虛函數?    不能!
<2>構造函數能否爲虛函數?      不能!
<3>析構函數能否爲虛函數?      可以!
<4>static函數能否爲虛函數?    不能!
答案已經公佈,原因請自己分析(提示:各種函數的特徵)

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