日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C++詳細講解繼承與虛繼承實現_C 語言

作者:m0_52012656 ? 更新時間: 2022-06-15 編程語言

繼承的概念及定義

概念:

繼承機制是面向對象程序設計為了提高代碼復用率的一種手段,它可以保持原類特性的基礎上進行拓展,簡單來說繼承是類層次的復用。

接下來我們來看一個簡單的繼承

class Person
{
public:
    void Print()
    {
        cout<<"name:"<<_name<<endl;
        cout<<"age:"<<_age<<endl;
    }
protected:
    string _name="zhao";
    int _age=18;
};
class Student : public Person
{
protected:
    int _stuid;
};
class Teacher :public Person
{
protected:
    int _jobid;
};

在上面這個類中繼承后父類(Person)的成員都會變成子類的一部分。

定義:

格式:

class 子類:

public 父類{ };

繼承關系和訪問限定符

繼承基類成員訪問方式的變化

類成員/繼承方式 public繼承 protected繼承 private繼承
基類的public成員 派生類的public成員 派生類的protected成員 派生類的private成員
基類的protected成員 派生類的protected成員 派生類的protected成員 派生類的private成員
基類的private成員 在派生類中不可見 在派生類中不可見 在派生類中不可見

總結

  • 基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
  • 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。
  • 實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
  • 使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。
  • 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。

基類和派生類對象賦值轉換

派生類對象可以賦值給基類的對象/指針/引用。這里有一個形象的書法叫做切片或切割

基類對象不能賦值給派生類對象

基類的指針可以通過強制類型轉換賦值給派生類的指針。但是必須是基類的指針是指向派生類對象時才是安全地。

繼承中的作用域

在繼承體系中基類和派生類都有獨立的作用域。

子類和父類中有同名成員;子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫做隱藏,也叫作重定義。如果要訪問父類的成員可以使用域作用限定符進行訪問。

注意函數構成隱藏的話只需要函數名相同。

實際在繼承體系里面最好不要定義同名的成員。

派生類的默認成員函數

在這里又把類與對象中學的六個默認成員函數拉出來了,那么在繼承體系中這幾個成員函數是如何生成的呢?

  • 構造函數:派生類的構造函數必須基類的構造函數初始化基類的那部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
  • 拷貝構造函數:派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
  • 賦值重載:派生類operator=必須要調用基類的operator=完成基類的賦值。
  • 析構函數:派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。

派生類對象初始化先調用基類構造再調派生類構造

派生類對象析構清理先調用派生類析構再調基類的析構。

簡單的運用:

class Person
{
public:
    Person(const  char* name="zhao")
        :_name(name)
        {
            cout<<"父構造"<<endl;
        }
    Person(const Person& p)
        :_name(p.name)
        {
            cout<<"父拷貝構造"<<endl;
        }
    Person& operator=(const Person& p)
    {
        cout<<"父賦值重載"<<endl;
        if(this!=&p)
            _name=p.name;
        return *this;
            
    }
    ~Person()
    {
        cout<<"父析構"<<endl;
    }
protected:
    string _name;
};
class Student:public Person
{
public:
    Student(const char* name,int num)
        :Person(name)
        ,_num(num)
    {
        cout<<"子構造"<<endl;
    }
    Student(const Student& s)
        :Person(s)
        ,_num(num)
    {
        cout<<"子拷貝構造"<<endl;
    }
    Student& operator=(const Student& s)
    {
        cout<<"子賦值重載"<<endl;
        if(this!=&s)
        {
        //小心這里是隱藏
            Person::operator=(s);
            _num=s._num;
        }
        return *this;
    }
    //需要注意在這塊~Student()和~Person()構成隱藏,這是由于多態的一些原因,任何類析構函數名都會被統一處理為destructor()
    ~Student()
    {
        cout<<"子析構"<<endl;
        //為了保證析構時,保持先子再父的后進先出的析構順序,子類析構函數完成后,會自動去調用父類的析構函數。
    }
protected:
    int _num;
    
};

繼承與友元

友元關系不能繼承,也就是說基類友元不是子類的友元。

繼承與靜態成員

基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員,無論派生出多少個子類,都只有一個static成員實例。

class Person
{
public :
    Person () {++ _count ;}
protected :
    string _name ; // 姓名
public :
    static int _count; // 統計人的個數。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
    int _stuNum ; // 學號
};
class Graduate : public Student
{
protected :
    string _seminarCourse ; // 研究科目
};
void TestPerson()
{
    Student s1 ;
    Student s2 ;
    Student s3 ;
    Graduate s4 ;
    cout <<" 人數 :"<< Person ::_count << endl;
    Student ::_count = 0;
    cout <<" 人數 :"<< Person ::_count << endl;
}

復雜的菱形繼承及菱形虛擬繼承

單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承

多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承

菱形繼承:菱形繼承是多繼承的一種特殊情況。

菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。在Assistant的對象中Person成員會有兩份。

虛擬繼承可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student和Teacher的繼承Person時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用

虛擬繼承解決數據冗余和二義性的原理為了研究虛擬繼承原理,我們給出了一個簡化的菱形繼承繼承體系,再借助內存窗口觀察對象成員的模型。

虛繼承原理

虛繼承實現:

在腰部兩個繼承之前加上關鍵字vittul,實現虛繼承。

class Animal{
    public:
    int _a;
}
class Tuo:virtual public Animal
{
    public:
    int _b;
}
class Sheep:virtual public Animal
{
    public:
    int _c;
}
class SheepTuo:public B ,public C
{
    public:
    int _b;
}

要探究虛繼承如何實現,需要借用VS的開發人員命令提示工具,在VS2019的工具->命令行->開發者命令提示中。cd到當前項目的目錄,輸入cl /d1reportSingleClassLayout"要查看的類名" “文件名”,在這里就是cl/d1reportSingleClassLayoutSheepTuo diamond_Inherit.cpp。可以看到當前類內存的結構。(編譯后才能查看到內存分布)

這個圖就是內存結構,可以看到,SheepTuo類中分別繼承了來自Sheep類的vbptr(虛基類指針)和Tuo類的vbptr(虛基類指針)。這個虛基類指針指向的是一個虛基類表,可以在圖中看到虛基類表中第一項存儲的是vbptr與本類的偏移地址,也就是繼承過來的Sheep類中初始位置就是存放Sheep類的的vbptr,在這里為0;第二項是本類的vbptr與虛基類的公有成員之間的偏移量,也就是Sheep的vbptr和Animal類的age之間偏移為8,Tuo的vbptr和age之間偏移量為4。對于虛基類的派生類,虛基類的偏移量由實際類型決定,因此在運行時才可以確定虛基類的地址。

指的注意的是,Sheep類中也是存放了一份age,在這里還可以看到,Sheep和Tuo的Size都是8,因為除了繼承的age以外,還有Size為4的虛函數指針

因為class SheepTuo :public Sheep, public Tuo繼承的時候,把Sheep和Tuo的vbptr都繼承了,然后通過他們類距離虛基類中的公共成員age的偏移量發現他們指向的是同一個age,所以就不會拷貝兩份,SheepTuo只保留一份age。至于虛繼承底層實現原理則與編譯器相關

繼承的總結

很多人說C++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜。所以一般不建議設計出多繼承,一定不要設計出菱形繼承。否則在復雜度及性能上都有問題。

多繼承可以認為是C++的缺陷之一,很多后來的OO語言都沒有多繼承,如Java

繼承和組合

public繼承是一種is-a的關系。也就是說每個派生類對象都是一個基類對象。

組合是一種has-a的關系。假設B組合了A,每個B對象中都有一個A對象。

優先使用對象組合,而不是類繼承 。

繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。

對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內部細節是不可見的。對象只以“黑箱”的形式出現。 組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于你保持每個類被封裝。

實際盡量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現多態,也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合

原文鏈接:https://blog.csdn.net/m0_52012656/article/details/123909354

欄目分類
最近更新