1. 程式人生 > >C++系列總結——繼承

C++系列總結——繼承

urn 地址 區分 自己 繼承 存在 虛函數 vat 位置

前言

前面講了封裝,但封裝只是隱藏了類內部實現。如果使用多態隱藏類本身的話,只有封裝是不夠的,還需要繼承。

繼承

通過封裝。我們把一些相關的函數和變量包裹在了一起,這些函數和變量就叫做類的成員函數成員變量。繼承就是一種獲取這個類的成員函數和成員變量的方式。通常,繼承了某個類的類叫做該類的派生類或者子類。

根據封裝的意義,當父類將部分成員函數和成員變量的訪問權限設置為private時,即使被繼承了,子類仍然無法訪問。

下面通過例子來說明一下,子類是如何保存這些繼承來的成員函數和成員變量的。

class A
{
public:
    A() : a( 1 )
    {
    }
    void foo()
    {
        std::cout << "A::foo()"  << std::endl;
        return;
    }
private:
    int a;
};
class B : public A
{
public:
    B() : b( 2 )
    {
    }
private:
    int b;
};
int main()
{
    B x;
    x.foo();
    return 0;
}

繼承可以是public、protected或private,不同關鍵字為父類設置了不同訪問權限。

  • public繼承意味著父類所有成員成員變量的訪問權限在子類中維持不變
  • protected繼承意味著父類中public的成員函數和成員變量在子類中變為protected
  • private繼承意味著父類中public和protected的成員函數和成員變量在子類中變為private

成員函數

因為類成員函數可以通過隱式參數this區分具體的調用對象,所以類成員函數只需要存在一份就可以。當子類繼承父類的成員函數時,子類只是得到了通過子類對象訪問父類成員函數的權利。

隱式參數意味著你沒有寫,但是編譯器幫你寫了。

當我們gdb調試上面的代碼時,會發現x.foo()實際調用的就是A::foo(),而不是A::foo()的一份拷貝。

   0x00000000004004fe <+8>:     lea    -0x10(%rbp),%rax   # -0x10(%rbp)就是x的地址
   0x0000000000400502 <+12>:    mov    %rax,%rdi # 將x的地址作為A::foo()的參數,也就是this
   0x0000000000400505 <+15>:    callq  0x400512 <A::foo()>

其實任何函數都只需要存在一份,成員函數只是一個稍微特殊的函數。

虛函數又是一個稍微特殊的成員函數,它也只存在一份,只不過是在調用上可能要多些操作,細節在講多態的時候再說。

成員變量

每個類的實例對象都要變更自己的成員變量,因此其空間肯定都是獨立的。當子類繼承父類的成員變量時,實際只是繼承了父類的數據結構。
當通過gdb打印x的值時,我們會發現它的結構如下

(gdb) p x
$1 = {<A> = {a = 1}, b = 2}

當我們直接查看x的地址的內容時,會發現A::aB::b就是連續排布的。

(gdb) x /2x &x
0x7fffffffe540: 0x00000001  0x00000002

也就是說當子類繼承父類時,子類的數據結構就是父類的成員變量加上子類自身的成員變量。

之所以父類的成員變量放在前,是因為父類指針可以直接指向子類,當以父類指針操作父類成員變量時就不需要額外進行地址偏移了。如果子類成員變量在前,那麽父類指針操作時還需要跳過子類成員變量。

我們大膽推測當子類繼承多個父類時,子類的數據結構就是寫在前的父類的成員變量加上寫在後的父類的成員變量再加上子類自身的成員變量,事實也確實如此。

多繼承時,因為存在多個父類數據結構,所以當不同的父類指針指向子類時,會進行一定偏移,保證該父類指針剛好指向自己的數據結構的起始位置。
多繼承會復雜化類關系圖,而且在一些場景下會帶來歧義,因此都不建議使用多繼承。反正我自己到現在為止都沒在實際項目中用過,只是在一些開源代碼中看到過。

結語

繼承除了是多態的基礎外,還是一種復用代碼的方式。但是謹記只有存在父子關系時才使用繼承,如果只是為了復用代碼的話,應當使用組合。

C++系列總結——繼承