1. 程式人生 > >C++總結5——繼承與多型【轉】

C++總結5——繼承與多型【轉】

一、繼承 

1.C++的繼承 

繼承有3種形式:私有繼承、保護繼承、公有繼承,預設的繼承方式是私有繼承。通常使用 public 繼承。

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

繼承是“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的記憶體佈局: 

這裡寫圖片描述

二、多型 

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函式能否為虛擬函式?    不能!  答案已經公佈,原因請自己分析(提示:各種函式的特徵)