1. 程式人生 > 實用技巧 >C++ 繼承和多型

C++ 繼承和多型

目錄

  1. 繼承的本質和原理
  2. 派生類的構造過程
  3. 過載,覆蓋,隱藏
  4. 靜態繫結 動態繫結
  5. 多型的vfptr 和 vftable
  6. 抽象類設計原理
  7. 多重繼承以及問題
  8. 虛基類 vbptr 和vbtable
  9. RTTI

一 繼承的本質和原理

  1. 基類給派生類提供統一的公共屬性(成員方法,成員屬性),通過繼承達到程式碼複用
  2. 基類可以給派生類提供統一的虛擬函式介面,派生類通過函式重寫,達到多型呼叫的目的

繼承方法,訪問許可權 public protected private 結論:

  1. 基類的private,無論哪種繼承方式,在派生類都可以繼承下來,但是無法訪問
  2. 基類的protected,派生類可以訪問,外部不能訪問
  3. 預設繼承方式: 如果是class 定義的 預設為private 若是struct 則是public

注意 :

class A {
 
    public:
      int ma;
    protected:
      int mb;
    private:
      int mc;
  };
  
  class B : private A {
  
    public:                                                                                                                                                
       
int md; protected: int me; private: int mf; }; class C : public B { public: int mg; protected: int mh; private: int mi; };

此時 類C中 對ma的訪問不再是 public 而是要看直接基類的訪問許可權,因為B 對A 的繼承是private 因此 在類C 中ma 訪問就是private。

二 派生類的構造過程

必須呼叫基類的建構函式構造 並且優先構造基類

構造與析構 的順序

1.先呼叫基類建構函式,構造從基類繼承來的成員
2.再呼叫派生類自己的建構函式,構造派生類自己的成員
3.先呼叫派生類自己的解構函式,釋放派生類自己佔用的外部資源
4.呼叫基類的解構函式,釋放基類部分成員佔用的外部資源

三 過載 隱藏 覆蓋

過載 : 在同一個作用域,函式名相同,引數列表不同,可構成過載函式

隱藏 :如果派生類與基類的同名函式,當呼叫派生類的函式時,預設會呼叫派生類的函式,發生了派生類函式對基類的函式隱藏,如果呼叫基類的同名函式,需要加上基類作用域

覆蓋 :指的是基類和派生類的同名函式,不但函式名相同,引數列表也相同,返回值也相同。並且基類是virtual 虛擬函式,那麼派生類的函式會被處理成虛擬函式,覆蓋是指在函式表中的函式地址的覆蓋

四 靜態繫結 動態繫結

class A {

public :

     void show() {cout<<"A::show"<<endl;}
     
     virtual void show(int a) { cout<<"A::show(int)<<endl;}

private:
    int ma;

};


class B : public A {

  public :
    void show() {cout<< " B:show()"<<endl}
    void show(int a) { cout<<"B::show(int)<<endl;}
private: int mb; }; int main() { B b; A * p = &b; p ->show(); // 靜態繫結 p->show(10); // 動態繫結  }

那麼 當呼叫show()時,此時p 是A* 型別 當呼叫方法show 時 就會去類A中 找相應的函式,在彙編層面就是call 0x1234 函式地址,並且在編譯時期就確定了函式地址,因此發生了靜態的繫結

當呼叫 show(10)時,先去類A 中找show(int )函式 但是A中的show(int) 是虛擬函式,那麼就訪問p指向的物件前4位元組 vfptr,然後訪問到vfptr指向的vftable,然後在vftable中找到虛擬函式地址在呼叫,並且只有在執行的時候才能獲取到虛擬函式地址,因此是發生的是動態繫結

p 是A型別 *p 識別的是RTTI 型別,因為A 中有虛擬函式,因此獲取vftable 獲取RTTI型別,如果A 中沒有虛擬函式,那麼*p 識別的是編譯期時期的型別

五 虛擬函式,vfptr,vftable

如果基類中Base有虛擬函式,那麼編譯期在編譯時期會為該型別生成一張虛擬函式表,虛擬函式表中放著虛擬函式的地址,還有RTTI 指標資訊,這張表在執行時被載入到.rodata,並且只能讀 不能寫。

那麼用這個Base類例項化的物件,前4 位元組會放著vfprt 虛擬函式指標,指向虛函表的地址

總結

1.類中出現虛擬函式,編譯階段會給該型別產生虛擬函式表,裡面存放了虛擬函式的地址和RTTI指標。
2.有虛擬函式的類例項化的物件,記憶體都多了一個vfptr虛擬函式指標,指向該物件型別的虛擬函式表,同類型物件都有自己的vfptr,但是它們共享一個vftable。
3.派生類如果提供了同名覆蓋函式,那麼在虛擬函式表中,需要把基類繼承來的虛擬函式地址給覆蓋掉。
4.一個類裡面出現多個虛擬函式,物件的記憶體只增長4個位元組(vfptr),但是虛擬函式表的大小會逐漸增大。

多型:

靜態時期的多型 : 模版,函式過載

執行時期的多型: 指基類指標指向派生類,呼叫派生類同名覆蓋函式,基類指標指向哪個派生類,就會呼叫該派生類的同名覆蓋函式,因為基類指標呼叫派生類的方法時,發生動態繫結,訪問了該物件指向的虛擬函式表,並且取出對應的虛擬函式地址,因此指向誰呼叫誰的方法

六 ,抽象類

class Animal
{
public:
    Animal(string name) :_name(name) {}
    virtual void bark() = 0; // 純虛擬函式
protected:
    string _name;
};

理論上類中擁有純虛擬函式的類就是抽象類,抽象類不能定義物件,但是可以定義指標和引用,

1.基類給所有派生類提供公共的屬性(成員變數)和方法(成員函式),通過繼承達到程式碼複用的目的。
2.基類可以給所有派生類提供統一的純虛擬函式介面,派生類通過函式重寫,達到多型呼叫的目的。

虛解構函式

class Base // 基類定義
{
public:
    Base(int data=10):_ptrb(new int(data))
    { cout << "Base()" << endl; }
    ~Base() { delete _ptrb; cout << "~Base()" << endl; }
protected:
    int *_ptrb;
};
class Derive : public Base // 派生類定義
{
public:
    Derive(int data=20):Base(data), _ptrd(new int(20))
    { cout << "Derive()" << endl; }
    ~Derive() { delete _ptrd; cout << "Derive()" << endl; }
private:
    int *_ptrd;
};
int main()
{
    Base *p = new Derive();
    delete p; // 只調用了Base的解構函式,沒有呼叫Derive派生類的解構函式
    return 0;
}

在delete p 的時候,因為基類的解構函式時普通函式,因此只是發生了靜態繫結,只調用了Base的解構函式,沒有呼叫Derive 的解構函式 尤其這種在堆上的物件,因此將基類解構函式修改成虛解構函式,這樣就是動態繫結,因此堆上的記憶體就能釋放

七,多重繼承

一 虛基類

class A {

public:
private:
   int ma;

};


class B : virtual public A {

public:
private:
   int mb;

};

被虛繼承的類就是虛基類 A為虛基類

B記憶體佈局

如果不被虛繼承 那麼B記憶體就是 A::ma , mb 那麼被虛繼承之後 將基類中成員搬到最後的位置,並且在原位置新增vbptr 指向虛基類表,在虛基類表(vbtable 記錄了虛基類的偏移量)

當基類指標指向派生類時,指標指向的永遠是基類成員的記憶體地址,因此存在虛基類 ,並且指向派生類物件在堆上,那麼不同編譯期析構可能出現記憶體洩漏

C++ 多重繼承

菱形繼承

這樣類D 具有了兩份類A 中 成員變數 存在問題

解決辦法 :

所以從A繼承的都要虛繼承 virtual 這樣 D中只具有A 中的一份資料 這樣A的建構函式 也是在D中呼叫,不會再是B,C呼叫