【Cpp】第十一章-繼承

繼承

什麼是繼承

  繼承是爲了更好的使代碼得以複用而產生的,同時呈現了面向對象程序設計中的層次結構,繼承會使得我們寫好的類可以得到擴展。簡單來說繼承可以增強我們的代碼複用,包括可以複用類的層次結構,同時使程序複用層次和條理。

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
    //如果我們設計一個類考慮到它會被繼承那麼最好用protected成員取代private成員
    //protected成員在類外部仍然是無法使用的,但是在派生類中它是可見的也就是可以使用的
    Person()
    {
        cout << "im a Person" << endl;
    }
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age = 20;
    string _name = "Misaki"; 
};
//Programer類,這裏讓其公有繼承Person類
//這時繼承產生的新類被稱爲子類,也叫派生類
//被繼承的舊類被稱爲父類,也叫基類
//繼承方式爲公有(public),其會讓基類中的public成員成爲派生類中的public成員
//基類中的protected成員成爲派生類中的protected成員,基類中的private成員在派生類中不可見
//關於繼承方式也在下文講解
class Programer: public Person
{
    //這裏我們不再重寫幾個默認生成的成員函數,他們默認會完成他們應有的功能
    //具體派生類的默認成員函數會在下文講解
protected:
    int _workyear;
};
class Teacher: public Person
{
protected:
    int _teachyear;
};
int main()
{
    Person person;//基類對象
    Programer programer;//派生類對象
    Teacher teacher;//派生類對象
    person.Print();
    programer.Print();
    teacher.Print();
}

im a Person
im a Person
im a Person
age = 20
name = Misaki
age = 20
name = Misaki
age = 20
name = Misaki

  以上這個例子就是簡單的利用繼承進行了類的擴展擴展出了兩個派生類,可以看出派生類中擁有積累的成員函數和成員變量,並且還可以添加新的成員函數和變量。

繼承方式

三種繼承方式

  繼承方式一共有三種:public, protected,private,這三種繼承方式都可以將基類中的成員繼承到派生類中,但是繼承方式的不同也決定着在派生類中繼承過來的成員的訪問權限的不同,具體關於各個繼承方式中對訪問權限的改變可以參考下圖。
繼承方式
  這裏提到基類中的private成員無論何種繼承方式在派生類中都是不可見的,所謂不可見指成員依然已經繼承了過來並且作爲了私有成員,但是在派生類中限制這些成員無論在類外還是類內都是無法訪問的。
  因此爲了讓一個成員在派生類中依然可以訪問便出現了protected訪問權限,所以才說如果一個類爲了方便繼承最好將其private訪問權限用protected來替代,這樣即使繼承產生派生類也依然可以在派生類中訪問該成員。
  如果我們在繼承時不寫出繼承方式,則class默認使用private繼承,struct默認使用public繼承,不過爲了可讀性最好還是顯示的寫出繼承方式。
  最爲經常使用的繼承方式是public繼承,它可以讓派生類中各個訪問權限下的成員在派生類中還爲相應的訪問權限,是最爲方便便於理解的繼承方式,並且其他繼承方式的擴展和維護性也並不強,因此除非特殊情況也不建議使用除public外的其他繼承方式。

切割

  在C++中允許將派生類對象隱式類型轉換爲父類。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    Person()
    {
        cout << "im a Person" << endl;
    }
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age = 20;
    string _name = "Misaki"; 
};
//派生類
class Programer: public Person
{
protected:
    int _workyear;
};
//派生類
class Teacher: public Person
{
protected:
    int _teachyear;
};
int main()
{
    Person person;
    //Programer programer;
    Teacher teacher;
    //person.Print();
    //programer.Print();
    //teacher.Print();
    //teacher是派生類對象,person是基類對象,這樣的隱式類型轉換是允許的
    person = teacher;
    //也可以用派生類對象來拷貝構造基類對象
    Person person2 = teacher;
    person.Print();
    person2.Print();
}


im a Person
im a Person
age = 20
name = Misaki
age = 20
name = Misaki

  以上這段代碼我們將派生類對象賦值給了基類對象,並且用派生類對象拷貝構造了基類對象,在這其中得以隱式轉換最重要的原因就是編譯器進行了切割處理,即將派生類對象中屬於基類的那一部分成員保留,而將屬於派生類的成員捨棄,由此才能完成類型轉換。
切割
  上圖過程即爲切割的過程,在派生類的對象/指針/引用賦值給父類的對象/指針/引用時都會發生隱式類型轉換,過程中都會發生切割。但是基類對象/指針引用不可以賦值給派生類對象/指針/引用。當然也有例外,基類指針可以通過強制類型轉換也可以賦值給派生類指針,當然只有在基類指針指向了派生類對象時纔是安全的。如果基類構成多態也可以用RTTI(運行時類型識別)的dynamic_cast類型識別後進行類型轉換,是最爲安全的。

繼承中的作用域

  在繼承體系中,基類和派生類都有屬於自己的作用域,那麼如果在派生類中定義了和基類同名的成員編譯器會怎樣處理呢?

重定義/隱藏

  派生類和基類有各自獨立的作用域,在派生類中如果出現和基類同名成員,則會優先調用派生類的同名成員,即隱藏基類成員,這個過程叫做被稱爲隱藏也叫做重定義
  但是我們也可以在派生類中加上域限定符顯示調用基類的成員。

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
    void Print()
    {
        cout << "Person::Print()" << endl;
    }
protected:
    int _age = 20;
    string _name = "Misaki"; 
};
//派生類
class Teacher: public Person
{
public:
    void Print()
    {
        cout << "Teacher::Print()" << endl;
    }
    void PrintAge()
    {
        cout << "Teacher::_age:" << _age << endl;//重名成員構成隱藏
        cout << "Person::_age:" << Person::_age << endl;//顯示調用基類成員
    }
protected:
    int _age = 19;
};
int main()
{
    Person person;
    person.Print();//Person::Print()
    Teacher teacher;
    teacher.Print();//Teacher::Print(),派生類重名成員構成隱藏
    teacher.PrintAge();
}


Person::Print()
Teacher::Print()
Teacher::_age:19
Person::_age:20

  值得注意的是,關於函數,函數重載指的是在相同作用域內函數名相同參數不同構成重載,而重定義/隱藏是指在基類和派生類的作用域內函數名相同就會構成隱藏。所以我們最好還是不要在派生類中定義和基類同名的成員,避免隱藏和重定義的發生。

派生類的默認成員函數

  派生類也有6個默認成員函數,但是着6個默認成員函數既要兼顧到基類也需要兼顧到派生類,因此在寫法上與常規的並不相同。

構造函數

  在派生類的構造函數中我們需要先顯示調用基類的構造函數對基類成員進行初始化然後才能對派生類成員進行初始化。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    //這裏構造函數就不選擇傳參了方便起見我都初始化爲定值
    Person()
        :_age(20)
        ,_name("Misaki")
    {
        cout << "Person()" << endl;
    }
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age;
    string _name;
};
//派生類
class Teacher: public Person
{
public:
    //派生類構造函數,先調用基類構造函數,在初始化派生類成員
    //默認生成的構造函數也是這樣實現的
    Teacher()
        :Person()
        ,_teachyear(3)
    {
        cout << "Teacher()" << endl;
    }
    void Print()
    {
        Person::Print();
        cout << "teachyear = " << _teachyear << endl;
    }
    static int _count;
protected:
    int _teachyear;
};
int Teacher::_count = 2;
int main()
{
    Teacher teacher;
    teacher.Print();
}


Person()
Teacher()
age = 20
name = Misaki
teachyear = 3

  從以上代碼可以得出派生類對象初始化會調用派生類構造函數,而在派生類構造函數中會先調用基類的構造函數對基類成員進行初始化然後纔會對派生類成員進行初始化。這樣我們就成功的完成了派生類構造函數,並且實現了成員初始化。

拷貝構造函數

  拷貝構造函數與構造函數類似,我們在派生類的拷貝構造函數中也需要先調用基類拷貝構造函數先拷貝構造基類成員再將派生類成員拷貝構造。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    //這裏構造函數就不選擇傳參了方便起見我都初始化爲定值
    Person()
        :_age(20)
        ,_name("Misaki")
    {
        cout << "Person()" << endl;
    }
    Person(const Person& person)
        :_age(person._age)
        ,_name(person._name)
    {
        cout << "Person(const Person&)" << endl;
    }
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age;
    string _name;
};
//派生類
class Teacher: public Person
{
public:
    //派生類構造函數,先調用基類構造函數,在初始化派生類成員
    //默認生成的構造函數也是這樣實現的
    Teacher()
        :Person()
        ,_teachyear(3)
    {
        cout << "Teacher()" << endl;
    }
    //拷貝構造函數,先調用基類構造函數對基類成拷貝構造,再拷貝構造派生類成員
    //默認生成的拷貝構造也是這樣的實現方法
    Teacher(const Teacher& teacher)
        :Person(teacher)//這裏利用派生類對象可以隱式轉換爲基類對象來調用基類拷貝構造
        ,_teachyear(3)
    {
        cout << "Teacher(const Teacher&)" << endl;
    }
    void Print()
    {
        Person::Print();
        cout << "teachyear = " << _teachyear << endl;
    }
    static int _count;
protected:
    int _teachyear;
};
int Teacher::_count = 2;
int main()
{
    Teacher teacher1;
    Teacher teacher2 = teacher1;
    teacher2.Print();
}


Person()
Teacher()
Person(const Person&)
Teacher(const Teacher&)
age = 20
name = Misaki
teachyear = 3

  從結果可以看出派生類對象在進行拷貝構造的時候調用了派生類拷貝構造函數,而派生類拷貝構造函數中會先調用基類拷貝構造函數對基類成員進行初始化然後纔會對派生類成員進行初始化

賦值運算符重載

  賦值運算符也同樣類似,需要先調用基類的賦值運算符重載,然後再進行派生類成員的賦值。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    //這裏構造函數就不選擇傳參了方便起見我都初始化爲定值
    Person()
        :_age(20)
        ,_name("Misaki")
    {
        cout << "Person()" << endl;
    }
    Person(const Person& person)
        :_age(person._age)
        ,_name(person._name)
    {
        cout << "Person(const Person&)" << endl;
    }
    Person& operator=(const Person& person)
    {
        if(&person != this)
        {
            _age = person._age;
            _name = person._name;
        }
        cout << "Person::operator=(const Person&)" << endl;
        return *this;
    }
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age;
    string _name;
};
//派生類
class Teacher: public Person
{
public:
    //派生類構造函數,先調用基類構造函數,在初始化派生類成員
    //默認生成的構造函數也是這樣實現的
    Teacher()
        :Person()
        ,_teachyear(3)
    {
        cout << "Teacher()" << endl;
    }
    //拷貝構造函數,先調用基類構造函數對基類成拷貝構造,再拷貝構造派生類成員
    //默認生成的拷貝構造也是這樣的實現方法
    Teacher(const Teacher& teacher)
        :Person(teacher)//這裏利用派生類對象可以隱式轉換爲基類對象來調用基類拷貝構造
        ,_teachyear(3)
    {
        cout << "Teacher(const Teacher&)" << endl;
    }
    //賦值運算符重載,先調用基類的賦值運算符重載對基類成員進行賦值,再賦值派生類成員
    //默認生成的賦值運算符重載也是這樣的實現方法
    Teacher& operator=(const Teacher& teacher)
    {
        if(&teacher != this)
        {
            Person::operator=(teacher);//調用基類的賦值運算符重載,並且用隱式類型轉換傳參
            _teachyear = teacher._teachyear;
        }
        cout << "Teacher::operator=(const Teacher&)" << endl;
        return *this;
    }
    void Print()
    {
        Person::Print();
        cout << "teachyear = " << _teachyear << endl;
    }
    static int _count;
protected:
    int _teachyear;
};
int Teacher::_count = 2;
int main()
{
    Teacher teacher1;
    Teacher teacher2;
    teacher2 = teacher1;
    teacher2.Print();
}


Person()
Teacher()
Person()
Teacher()
Person::operator=(const Person&)
Teacher::operator=(const Teacher&)
age = 20
name = Misaki
teachyear = 3

  從以上代碼可以看出派生類在賦值時會先調用派生類賦值運算符重載,而在派生類賦值運算符重載中會首先調用積累的賦值運算符重載對基類成員進行賦值,然後纔會賦值派生類成員

析構函數

  析構函數與其他的默認成員函數有所不同,因爲派生類在構造時是先初始化基類成員再初始化派生類成員,因此在析構時是先釋放派生類成員再釋放基類成員。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    //這裏構造函數就不選擇傳參了方便起見我都初始化爲定值
    Person()
        :_age(20)
        ,_name("Misaki")
    {
        cout << "Person()" << endl;
    }
    Person(const Person& person)
        :_age(person._age)
        ,_name(person._name)
    {
        cout << "Person(const Person&)" << endl;
    }
    Person& operator=(const Person& person)
    {
        if(&person != this)
        {
            _age = person._age;
            _name = person._name;
        }
        cout << "Person::operator=(const Person&)" << endl;
        return *this;
    }
    ~Person()
    {
        cout << "~Person()" << endl;
    }
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age;
    string _name;
};
//派生類
class Teacher: public Person
{
public:
    //派生類構造函數,先調用基類構造函數,在初始化派生類成員
    //默認生成的構造函數也是這樣實現的
    Teacher()
        :Person()
        ,_teachyear(3)
    {
        cout << "Teacher()" << endl;
    }
    //拷貝構造函數,先調用基類構造函數對基類成拷貝構造,再拷貝構造派生類成員
    //默認生成的拷貝構造也是這樣的實現方法
    Teacher(const Teacher& teacher)
        :Person(teacher)//這裏利用派生類對象可以隱式轉換爲基類對象來調用基類拷貝構造
        ,_teachyear(3)
    {
        cout << "Teacher(const Teacher&)" << endl;
    }
    //賦值運算符重載,先調用基類的賦值運算符重載對基類成員進行賦值,再賦值派生類成員
    //默認生成的賦值運算符重載也是這樣的實現方法
    Teacher& operator=(const Teacher& teacher)
    {
        if(&teacher != this)
        {
            Person::operator=(teacher);//調用基類的賦值運算符重載,並且用隱式類型轉換傳參
            _teachyear = teacher._teachyear;
        }
        cout << "Teacher::operator=(const Teacher&)" << endl;
        return *this;
    }
    //要注意派生類析構函數不需要顯示調用基類的析構函數,在調用派生類析構函數釋放派生類成員後
    //會自動調用基類的析構函數,來滿足先釋放派生類成員再釋放基類成員的順序
    ~Teacher()
    {
        cout << "~Teacher()" << endl;
        //Person::~Person();這樣調用會報錯
    }
    void Print()
    {
        Person::Print();
        cout << "teachyear = " << _teachyear << endl;
    }
    static int _count;
protected:
    int _teachyear;
};
int Teacher::_count = 2;
int main()
{
    Teacher teacher;
}


Person()
Teacher()
~Teacher()
~Person()

  派生類析構函數執行時會先釋放派生類成員再釋放基類成員,並且要注意派生類析構函數會自動調用基類析構函數進行清理,無需手動調用。從執行結果上也可以看出編譯器是先釋放派生類成員再釋放基類成員。

總結

  對派生類默認成員函數進行總結。
  1、派生類構造函數必須首先調用基類構造函數構造基類成員,之後再構造派生類成員才能成立。
  2、派生類拷貝構造函數必須先調用基類拷貝構造函數拷貝構造基類成員,之後再拷貝構造派生類成員才能成立。
  3、派生類賦值運算符重載必須先調用基類賦值運算符重載函數對基類成員進行賦值,再對派生類成員進行賦值才能成立。
  4、派生類析構函數會在調用後自動調用基類析構函數無需顯式調用基類析構函數。
  5、派生類對象構造會先構造基類成員再構造派生類成員。
  6、派生類對象析構會先釋放派生類成員再釋放基類成員。

繼承與友元

  在C++類和對象中有介紹友元這一概念,即允許在友元類或友元函數中打破類的封裝從而使用類的保護和私有成員。那麼如果涉及繼承,編譯器會如何處理呢?
  友元關係不能繼承,即基類的友元不能訪問派生類的私有和保護成員,但其依然可以訪問派生類中從基類繼承而來的私有和保護成員

#include <iostream>
#include <string>
using namespace std;
class Test;
//基類
class Person
{
    friend class Test;
public:
    void Print()
    {
        cout << "age = " << _age << endl;
        cout << "name = " << _name << endl;
    }
protected:
    int _age = 20;
    string _name = "Misaki";
};
//派生類
class Teacher: public Person
{
public:
    void Print()
    {
        Person::Print();
        cout << "teachyear = " << _teachyear << endl;
    }
protected:
    int _teachyear = 3;
};
class Test
{
public:
    void Print()
    {
        cout << "age = " << person._age << endl;
        cout << "name = " << person._name << endl;
    }
    void Print2()
    {
        cout << "age = " << teacher._age << endl;
        cout << "name = " << teacher._name << endl;
        //cout << "teachyear = " << teacher._teachyear << endl;//無法訪問
    }
private:
    Person person;
    Teacher teacher;
};
int main()
{
    Test test;
    test.Print2();
}


age = 20
name = Misaki

繼承和靜態成員

  關於繼承和靜態成員,我們所要記住的只有一句話,在整個繼承體系中,無論發生多少次繼承,靜態成員在繼承體系中只存在一份

靜態成員函數

  關於靜態成員函數,這句話將更好理解,在不發生隱藏的情況下我們調用的依然是基類的靜態函數。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    static void Print()
    {
        cout << "Person::_count = " << _count << endl;
    }
protected:
    int _age = 20;
    string _name = "Misaki"; 
    static int _count;
};
int Person::_count = 1;
//派生類
class Teacher: public Person
{
public:
    //static void Print()
    //{
    //    cout << "Teacher::_count = " << _count << endl;
    //}
protected:
    int _age = 19;
};
int main()
{
    Person::Print();
    Teacher::Print();
}


Person::_count = 1
Person::_count = 1

  在發生隱藏的情況下,就會調用派生類的靜態函數。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    static void Print()
    {
        cout << "Person::_count = " << _count << endl;
    }
protected:
    int _age = 20;
    string _name = "Misaki"; 
    static int _count;
};
int Person::_count = 1;
//派生類
class Teacher: public Person
{
public:
    static void Print()
    {
        cout << "Teacher::_count = " << _count << endl;
    }
protected:
    int _age = 19;
};
int main()
{
    Person::Print();
    Teacher::Print();
}


Person::_count = 1
Teacher::_count = 1

靜態成員變量

  關於靜態成員變量,我們要理解其在整個繼承體系中有且只有一份,我們在基類中將其改變,派生類的也會跟着改變。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    static void Print()
    {
        cout << "Person::_count = " << _count << endl;
    }
    static int _count;
protected:
    int _age = 20;
    string _name = "Misaki"; 
};
int Person::_count = 1;
//派生類
class Teacher: public Person
{
public:
    static void Print()
    {
        cout << "Teacher::_count = " << _count << endl;
    }
protected:
    int _age = 19;
};
int main()
{
    Person::_count = 3;
    Person::Print();
    Teacher::Print();
}


Person::_count = 3
Teacher::_count = 3

  但是如果我們在派生類中定義同名靜態成員變量構成隱藏的話,則會產生新的變量,並且隱藏掉基類的同名靜態成員,當然我們也可以通過顯示調用的方式再去調用它。

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    static void Print()
    {
        cout << "Person::_count = " << _count << endl;
    }
    static int _count;
protected:
    int _age = 20;
    string _name = "Misaki"; 
};
int Person::_count = 1;
//派生類
class Teacher: public Person
{
public:
    static void Print()
    {
        cout << "Teacher::_count = " << _count << endl;
        cout << "Person::_count = " << Person::_count << endl;
    }
protected:
    int _age = 19;
    static int _count;
};
int Teacher::_count = 5;
int main()
{
    Person::_count = 3;
    Person::Print();
    Teacher::Print();
}


Person::_count = 3
Teacher::_count = 5
Person::_count = 3

繼承中隱藏/重定義的權限及生命週期

  我們知道了靜態成員發生隱藏時纔會產生新的一份空間進行存儲,如果我們將原本在基類中是靜態成員的變量在派生類中定義同名成員爲非靜態的成員編譯器會怎麼判定呢?

#include <iostream>
#include <string>
using namespace std;
//基類
class Person
{
public:
    static void Print()
    {
        cout << "Person::_count = " << _count << endl;
    }
    static int _count;
protected:
    int _age = 20;
    string _name = "Misaki"; 
};
int Person::_count = 1;
//派生類
class Teacher: public Person
{
public:
    static void Print()
    {
        //cout << "Teacher::_count = " << _count << endl;//編不過了,因爲靜態函數中只能調用靜態成員,而此時_count隱藏已經不是靜態成員
        cout << "Person::_count = " << Person::_count << endl;
    }
    int _count = 5;
protected:
    int _age = 19;
};
int main()
{
    Person::_count = 3;
    Person::Print();
    Teacher::Print();
    Teacher teacher;
    cout << teacher._count << endl;//可以訪問了
}


Person::_count = 3
Person::_count = 3
5

  由此我們可以判斷一點,如果在派生類中發生隱藏和重定義那麼在派生類中該同名成員的生命週期及訪問權限將根據在派生類中重定義的情況而決定,因此在基類中即使是靜態的成員如果在派生類中重定義爲非靜態變量進行使用也是可以的,可見重定義和隱藏是十分恐怖的,因此我們最好是儘量防止在派生類中定義和基類重名的成員,避免發生隱藏和重定義。

菱形繼承和虛繼承

  Cpp語法複雜這一特點可以從繼承中體現出來,因爲其支持多繼承,而多繼承就有可能會產生菱形繼承的情況。

菱形繼承

  什麼是菱形繼承呢?其產生原因就要“歸功於”Cpp支持多繼承這一特點,多繼承就是允許一個類繼承於多個基類,當派生類多繼承於多個基類時,這些基類也有可能繼承於更上層的同一個基類,由此就會產生菱形繼承的情況。

#include <iostream>
using namespace std;
class A
{
public:
    char _a = 'A';
};
//B繼承於A
class B: public A
{
public:
    char _b = 'B';
};
//C也繼承於A
class C: public A
{
public:
    char _c = 'C';
};
//D繼承於B,C
class D: public B, public C
{
public:
    char _d = 'D';
};

  以上這種結構就會產生菱形繼承,我們用畫圖的方式具現化表示一下。
菱形繼承
  這種繼承體系就是菱形繼承,也是非常直觀的可以體現出來的。

菱形繼承帶來的問題

  菱形繼承是Cpp多繼承所帶來的主要問題之一,一旦發生菱形繼承,首當其衝的我們就應該考慮到菱形繼承帶來的數據冗餘數據二義性的問題。
  還用上面的例子,因爲D同時繼承了B,C類,而B,C類又都分別繼承了A類這就意味着D中存在着兩份A類,一份是從B那裏繼承來的,另一份是從C繼承來的,這就造成了數據冗餘,並且當我們通過D想要直接訪問A類成員時編譯器會報錯,因爲編譯器不知道你要訪問的時B中的A類成員還是C中的A類成員,我們必須加上域限定符才能訪問,這就造成了數據二義性,但是我們通過域限定符姑且可以解決數據二義性的問題,但是數據冗餘卻無法解決。

#include <iostream>
using namespace std;
class A
{
public:
    char _a = 'A';
};
//B繼承於A
class B: public A
{
public:
    char _b = 'B';
};
//C也繼承於A
class C: public A
{
public:
    char _c = 'C';
};
//D繼承於B,C
class D: public B, public C
{
public:
    char _d = 'D';
};
int main()
{
    D d;
    //cout << d._a << endl;//報錯
    d.B::_a = 'E';
    d.C::_a = 'F';
    cout << d.B::_a << endl;
    cout << d.C::_a << endl;
}


E
F

  從以上結果中可以看出d中有着兩份A類的成員,因此在我們日常程序設計中應該極力避免菱形繼承的產生,因爲會浪費空間也會產生不必要的錯誤。

虛擬繼承

  但是如果在某些場景下一定要產生菱形繼承,我們也有辦法避免數據冗餘及數據二義性的發生,這就要牽扯到虛擬繼承

#include <iostream>
using namespace std;
class A
{
public:
    char _a = 'A';
};
//使用虛擬繼承使B繼承於A
class B: virtual public A
{
public:
    char _b = 'B';
};
//使用虛擬繼承使C繼承於A
class C: virtual public A
{
public:
    char _c = 'C';
};
//D繼承於B,C
class D: public B, public C
{
public:
    char _d = 'D';
};
int main()
{
    //一旦使用虛擬繼承那麼D類中就只存在一份A的成員變量
    D d;
    //無論用什麼作用域進行訪問都只能訪問到同一份
    d.B::_a = 'E';
    cout << d._a << endl;
    cout << d.B::_a << endl;
    cout << d.C::_a << endl;
    d.C::_a = 'F';
    cout << d._a << endl;
    cout << d.B::_a << endl;
    cout << d.C::_a << endl;
    //我們甚至還可以這樣訪問,一旦使用虛擬繼承我們可以視d間接繼承了A,因此也有了A的作用域
    //這在不使用虛擬繼承的情況下是無法完成的
    cout << d.A::_a << endl;
}



E
E
E
F
F
F
F

  那麼虛擬繼承是如何做到的這一切呢?在不適用虛擬繼承的情況下類D中可以這樣表示。
虛擬繼承
  但是引入虛擬繼承後,編譯器就會生成一張虛基表,這張表中存放着這個類與其虛擬繼承的基類之間的地址偏移量,而在這個類中也會多生成一個指針被稱爲虛基表指針指向這張虛基表。因此此時D類中由於所繼承的兩個基類都是虛擬繼承自A類,因此B,C類都會有屬於自己的虛基表及指向這張表的虛基表指針,而他們的虛基表中都會存儲與同一個A類之間的地址偏移量,因此就可以做到D類中就有唯一的一份A類成員。
  使用虛擬繼承D中可以如下表示。
虛擬繼承
  但是要注意虛基表中存放的並不是直接指向類A的指針,而是與A的地址偏移量,由此可以找到A。這樣就確保了D中只有唯一的一份A的成員,解決了數據冗餘以及數據二義性的問題。

int main()
{
    cout << sizeof(B) << endl;
}


8

  我們取一個虛擬繼承的類的大小也可以發現其大小變成了8,這是因爲多了一個虛基表指針佔了4個字節,加上基類個派生類的兩個字符型成員各佔2字節,還有2個字節的補齊成了8。不過這樣我們知道了虛基表指針是存儲在對象中的,那麼虛基表存儲在哪裏呢?這個根據每個編譯器的不同都有不同的處理,vs是存儲在寄存器中的。
  這裏還有一點要注意的,雖然一個繼承體系中虛繼承自同一個基類的派生類一共只存儲一份基類,這是爲了防止數據冗餘,但是在sizeof()計算每個派生類大小時編譯器也是會將基類大小的計算加入每個派生類中的,儘管他們一共只存儲一份基類。例如上面的例子如果我們將B/C類屬於他們自身的成員去掉我們會發現他們的大小還是有8,這就是因爲編譯器在每個派生類中還加入了基類大小的計算,這點在虛繼承上也不例外。

總結

  在繼承這一章,我們透徹的學習了繼承的各種知識點,並且還解析了菱形繼承以及虛擬繼承底層的實現原理,我們不由得產生了一個問題,我什麼時候使用繼承什麼時候使用組合呢?
  繼承:是一種is a的關係。
  組合:是一種has a的關係。
  這一點並不難理解,但是實際開發中,我們儘量優先使用組合,因爲組合耦合度低,易於開發和維護,我們可以不用過多的考慮基類的實現,而繼承不同。但是有一些情況下確實是繼承更爲符合情景呢麼就是用繼承,但要小心多繼承。繼承的使用也是十分廣泛的,比如之後要學習的多態,都多虧於繼承的語法,當然如果繼承和組合都可以使用的話還是使用組合更好。

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