對繼承的纏纏綿綿

1.繼承

1.1繼承概念:
繼承是面向對象重要的複用手段,就是一個類繼承另一個類的屬性和方法,這個新類包含上一個類的屬性和方法,被稱爲子類或者派生類,被繼承的類叫做父類或者基類。
1.2繼承的方式及訪問屬性

1.2.1共有繼承(public):除過基類是私有成員無法繼承外,其它成員都可以按照基類方式繼承,基類的私有成員仍然是私有的,不能被這個派生類的子類所訪問。

1.2.2保護繼承(protected):除過基類是私有成員外,其它成員繼承後都變爲保護繼承,並且只能被它的派生類成員函數或友元訪問,基類的私有成員仍然是私有的。

1.2.3私有繼承(private):私有繼承的特點是基類的公有成員和保護成員都作爲派生類的私有成員,並且不能被這個派生類的子類所訪問。

1.3三種繼承關係

繼承方式 基類是public 基類是protected 基類是private 繼承引起的訪問關係變化
public 仍爲public成員 仍爲protected 不可見 基類的非私有成員在子類的訪問屬性都不變
protected 變爲protected 仍爲protected 不可見 基類的非私有成員在子類中都變爲保護成員
private 變爲private 變爲privated 不可見 基類中的私有成員都變爲子類的私有成員
eg:
class father//父類
{
    public:
    char* _fname;//父親的名字
    protected:
    int _fIdCard;//銀行卡賬號是需要保護的,子類就是兒子可以知道
    privateint _fIdPassword;//銀行卡密碼只能父類知道,子類不能知道,怕你亂取錢花
}
class son:public father//子類
{
    public:
    void func(void)
    {
        char* _sname = _fname;//可以繼承,基類的公有成員在派生類中變爲公有成員。
        int _sIdCard = _fIdCard;//可以繼承,基類的保護成員在派生類中變爲保護成員
        int _sIdPassword = _fIdPassword;//不能繼承,基類的私有成員在派生類中是不可見的
    }
}

知識點總結:
1.基類中的私有成員不想讓基類對象直接訪問基類成員,但派生類卻可以訪問,就定義爲保護成員,可見保護成員是爲繼承而出現的
2.public繼承保持is-a原則,每個父類可用的成員在子類中也可用。當然不包括私有成員。
3.protetced/private繼承,基類的部分成員並未完全成爲子類接口的一部分,是 has-a 的關係原則,所以一般情況下不會使用
這兩種繼承關係,在絕大多數的場景下使用的都是公有繼承
5.不管是哪種繼承方式,在派生類內部都可以訪問基類的公有成員和保護成員,但是基類的私有成員存在但是在子類中不可見(不能
訪問)
6.使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。

附:
is-a原則: 全稱爲is-a-kind-of,顯然這裏is-a原則指的是子類是父類的一種;例如:人是基類,男人是子類,子類和基類構成繼承關係;滿足is-a 原則;男人是人的一類
has-a 原則:protected/private繼承是實現繼承,父類的成員有一部分子類是無法繼承的 一個子類中有一個父類,即部分父類的成員是不可用的(has-a),has-a多用於組合關係。
例如:學生是一個類,姓名,學號,電話號都是一個類,這三個類組合成爲學生類。


2.繼承與轉換

2.1賦值兼容規則–public繼承

2.1.1 :子類對象可以賦值給父類對象(因爲子類是從父類繼承下來的,因此父類肯定會包含部分子類成員,在子類進行賦值時,父類成員只要獲取自己的部分成員即可,這就叫做切片處理)
2.1.2:父類對象不能賦值給子類對象(因爲父類私有成員是不可見的,所以父類給子類賦私有成員時,子類並不知道需要多少空間來接收父類私有成員)
2.1.3 :父類的指針/引用可以指向子類對象(但是隻能訪問子類從父類繼承過來的成員,訪問子類其它成員函數或者變量會出錯)
2.1.4 :子類的指針/引用不能指向父類對象(因爲當你用指針訪問父類中子類特有的成員函數或者變量時,父類中沒有就會非法訪問,但是可以通過強制類型轉換完成)

eg:
class base
{
    public:
    char* name;
    int number;
}
class derived: public Base
{
    publicint IDcard;
}

base b;//基類
derived d;//派生類

b = d;//基類對象不能賦給派生類對象
d = b;//派生類對象可以賦給基類對象

//基類的指針或引用可以指向派生類對象
base *b1 = &d;
base &b2 = d;

//派生類的指針或引用不能指向基類對象(但可以通過強制轉換)
derived *b3 = (derived*)&d;
derived &b4 = (derived&)d;

這裏寫圖片描述


3.成員函數的重載、 覆蓋和隱藏區別

3.1.、重載:
在同一個作用域裏,函數名字相同,參數類型不同或者參數個數不同或者返回值不同,會構成重載。
c語言中函數名字相同是不能實現的,但是在C++中卻可以,因爲 c++ 中底層彙編語言中會將重載函數名映射爲返回類型+函數名+參數列表。這樣c++有同名函數時,鏈接時只要參數類型或者順序不一樣就可以調用
深入瞭解重載點擊這裏
3.2、重寫(覆蓋):
1. 不在同一個作用域(即一個父類一個子類)
2. 函數名相同/參數完全相同(包括類型和順序)/返回值相同(協變除外)
3. 基類函數前必須有virtual。(即基類函數爲虛函數)
3.3、重定義(隱藏):
1.在不同作用域裏(即父類和子類)
2.子類和父類函數名相同,參數不同,在子類中父類的成員函數被隱藏
3.子類和父類函數名相同,參數完全相同,但父類函數沒有virtual(即不是虛函數),父類成員函數被隱藏,切記不是重寫
舉個栗子:

class B
{
public:


    void fun()
    {
        cout << "B::fun()" << endl;
    }
    void fun(int a)
    {
        cout << "B::fun(a)" << endl;
    }
};
class D : public B
{
public:

    void fun()
    {
        cout << "D::fun()" << endl;
    }
};

這裏寫圖片描述

class B
{
public:


    void fun()
    {
        cout << "B::fun()" << endl;
    }
    void fun(int a)
    {
        cout << "B::fun(a)" << endl;
    }
    virtual void fun1()
    {
        cout << "B::fun1()" << endl;
    }
};
class D: public B
{
public:

    using B::fun;//讓class B類中所有fun的所有函數在D類中都可見,並且是public
    void fun()
    {
        cout << "D::fun()" << endl;
    }
    virtual void fun1()
    {
        cout << "D::fun1()"<<endl;
    }

};

這裏寫圖片描述

class B
{
public:


     void fun()
    {
        cout << "B::fun()" << endl;
    }
     void fun(int a)
    {
        cout << "B::fun(a)" << endl;
    }
    virtual void fun1()
    {
        cout << "B::fun1()" << endl;
    }
};
class D : private B
{
public:

     void fun(int a)//轉交函數
    {
        B::fun(2);//偷偷成爲inline內聯函數
    }
    void fun1()
    {
        cout << "D::fun1()" << endl;
    }

}

這裏寫圖片描述
總結:
1.子類繼承父類時,它們有重名函數,子類成員函數將屏蔽父類對子類成員函數直接訪問。
2.子類對象想要訪問同名父類函數時可以通過顯示調用,using表達式聲和轉交函數
3.但在實際使用時子父類最好不要用同名函數,以免出錯。

4.派生類成員函數

  1. 子類繼承父類,但是並不是父類所以成員函數和變量子類都可以繼承,那麼那些不能繼承呢?

    子類不能從父類繼承的有:
    1、構造函數
    原因:子類創建對象時,需要調用父類的構造函數,如果你繼承了父類的構造函數,就不能讓子類的構造函數去初始化屬於父類的那部分成員變量了,也就是說,子類裏屬於父類部分的成員函數必須由父類的構造函數親自初始化,所以不能繼承
    2.、析構函數
    原因:析構函數繼承會構成重寫。
    3、賦值操作符=重載函數
    原因:因爲賦值操作符重載函數的作用是自己拷貝自己的類,如果繼承下來,拷貝類型就對不上號了,就會出錯。

  2. 子類的構造函數應該在其初始化列表裏顯式的調用父類構造函數(除非父類構造函數不能訪問)

  3. 如果父類是多態類,那麼必須把父類析構函數定義爲虛函數,因爲這樣就可以像其他虛函數一樣實現動態綁定了,否則就會產生內存泄漏。
    詳細原因戳這裏
  4. 在寫子類的賦值函數時,注意不要忘記父類的數據成員重新賦值,這可以通過調用父類的賦值函數來實現

那麼舉個栗子吧

class Base
{
public:
    Base(const char* name = "")
        :_name(name)
    {
        cout << "Base()" << endl;
    }

    Base(const Base &b)
        :_name(b._name)
    {
        cout << "Base(const Base &b)" << endl;
    }

    Base& operator = (const Base &b)
    {
        cout << "Base& operator = (const Base &b)" << endl;
        if (this != &b)
        {
            _name = b._name;
        }
        return *this;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    string _name;
};

class Derived : public Base
{
public:

    Derived(const char* name = "", int number = 0)
        :Base(name)//顯示調用構造函數
        ,_number(number)
    {
        cout << "Derived(const char* name="", int number=0)" << endl;
    }

    Derived(const Derived& d)
        :Base(d)//顯示調用拷貝構造
        ,_number(d._number)
    {
        cout << "Derived(const Derived& d)" << endl;
    }
    Derived& operator = (const Derived& d)
    {
        if (this != &d)
        {
            Base::operator=(d);//對基類的成員重新賦值
            _number = d._number;
            cout << "Derived& operator = (const Derived&d)" << endl;
        }
        return *this;
    }
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
    int _number;
};

這裏寫圖片描述
可以看出在派生類構造對象時,先構造基類成員函數再構造派生類成員函數,但析構時,缺失卻是先析構派生類再析構父類。


5.菱形繼承和虛繼承

5.1、菱形繼承
:兩個子類繼承父類,又一個子類同時繼承上面兩個子類
看圖:
這裏寫圖片描述
菱形繼承存在問題:
5.1.1:數據冗餘
5.1.2:二義性
eg:

class person
{
public:

    int _name;//爲了後續更好驗證,把所有成員變量定義爲int

};

class student :public person
{
public:

    int _number;//學號

};

class teacher :public person
{
public:

    int id;//學工號
};

class assistant :public student,public teacher
{

public:

    int telephone;

};

這裏寫圖片描述
這裏寫圖片描述

解決辦法:
1.指定作用域

void Test()
{
    assistant a;
    a.teacher::_name;
    a.student::_name;
}

2.採用虛繼承

class person
{
public:

    int _name;

};

class student : virtual public person
{
public:

    int _number;//學號

};

class teacher : virtual public person
{
public:

    int _id;//學工號
};

class assistant :public student,public teacher
{

public:

    int _telephone;

};

1.虛繼承寫時要注意: virtual public person這樣寫,如果寫成virtual person,那就默認虛繼承的私有繼承了,你就不能訪問父類成員變量
2.虛繼承解決了菱形繼承中的數據冗餘造成的浪費空間問題


接下來分析虛繼承怎麼解決掉這些問題的
先看看虛繼承後assistant a對象大小的改變
這裏寫圖片描述

普通繼承中,assistant繼承teacher的8字節和student8字節,再加上自己的4字節,所以大小時20字節,那虛繼承多出來的這四個字節是什麼?我們來看看內存中的情況

這裏寫圖片描述

可以看出每一次虛繼承後,子類都會產生一個指針,指針指向一個虛基表,虛基表裏的內容是一個偏移量,是子類對象實例化後通過自身地址加上這個偏移量找到存放繼承自父類對象的地址,這樣就可以找到裏面的內容了。

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