天天看點

[ C++ ] 繼承

問題引入:

        面向對象程式設計的主要目的之一是提供可重用的代碼,C++提供了一個方法來擴充和修改類。這種方法叫做類繼承。它能夠從已有的類派生出新的類,而派生類繼承了原有類(稱為基類)的特征,包括方法。

1.繼承的概念和定義

1.1繼承的概念

繼承(inheritance)機制是面向對象程式設計使代碼可以複用的最重要的手段,它允許程式員在保

持原有類特性的基礎上進行擴充,增加功能,這樣産生新的類,稱派生類。繼承呈現了面向對象

程式設計的層次結構,展現了由簡單到複雜的認知過程。以前我們接觸的複用都是函數複用,繼

承是類設計層次的複用。

一個簡單的基類:

        從一個類派生出另一個類時,原始類稱為基類,繼承類稱為派生類。為說明繼承,首先需要一個基類。Person類中有_name,_age成員變量,有Print()成員方法。接下來我們将派生一個Student學生類

//父類、基類
class Person
{
public:
  void Print(){
    cout << "name :" << _name << endl;
    cout << "age :" << _age << endl;
  }

//private:
//protected:
  string _name = "Peter";
  int _age = 18; 
};      
[ C++ ] 繼承

 派生一個類:

        派生類的聲明方式,首先将Student類聲明為從Person類派生而來:

class Student : public      
[ C++ ] 繼承

        冒号指出Student類的基類是Person類。public表示Person是一個公有基類,這被稱為公有派生。派生類對象包含基類對象。使用公有派生,基類的公共成員将成為派生類的公有成員;基類的私有部分也将成為派生類的一部分,但隻能通過基類的公有和保護方法通路。換句話說,Student類定義的對象s,可以使用Person類中的公有方法。

        那麼Student對象具有以下特征:

                1.派生類對象存儲了基類的資料成員(派生類繼承了基類的實作)。

                2.派生類對象可以使用基類的方法(派生類繼承了基類的接口)。

        派生類需要添加什麼呢?

                1. 派生類需要自己的構造函數。

                2.派生類可以根據需要添加額外的資料成員和成員函數。

那麼我們現在寫一個Student類:

class Student : public Person
{
public:
  void func(){
    Print();
  }
protected:
  int _stuid;//學号      
[ C++ ] 繼承

        我們可以看到Student類繼承了Person類,并且是公有繼承,并且Student類擁有自己額外的成員變量以及成員函數。

[ C++ ] 繼承
[ C++ ] 繼承

1.2 繼承定義

1.2.1定義格式

上文我們所提到的,Person類是基類(父類),Stundent是派生類(子類)。繼承方式是公有繼承。

[ C++ ] 繼承
[ C++ ] 繼承

1.2.2 繼承關系和通路限定符

繼承方式不僅有我們剛提到的公有繼承(public),還有保護繼承(protected)以及私有繼承(private),在類中,通路限定符可以選擇性的将其接口提供給外部的使用者使用。是以類内有3中方式,類繼承有3中方式,這樣就組成了9中的通路關系。

[ C++ ] 繼承
[ C++ ] 繼承

1.2.3 繼承基類成員通路方式的變化

類成員/繼承方式 public繼承 protected繼承 private繼承

基類的public

成員

派生類的public

成員

派生類的protected

成員

派生類的private

成員

基類的protected成員 派生類的protected成員

派生類的protected

成員

派生類的private

成員

基類的private

成員

在派生類中

不可見

在派生類中

不可見

在派生類中

不可見

總結:

1. 基類private成員在派生類中無論以什麼方式繼承都是不可見的。這裡的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是文法上限制派生類對象不管在類裡面還是類外面都不能去通路它。

2.基類private成員在派生類中是不能被通路,如果基類成員不想在類外直接被通路,但需要在派生類中能通路,就定義為protected。可以看出保護成員限定符是因繼承才出現的。

3. 實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的通路方式 == Min(成員在基類的通路限定符,繼承方式),public > protected

> private。

4.使用關鍵字class時預設的繼承方式是private,使用struct時預設的繼承方式是public,不過

最好顯示的寫出繼承方式。

5.在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡

使用protetced/private繼承,因為protetced/private繼承下來的成員都隻能在派生類的類裡

面使用,實際中擴充維護性不強。

 代碼示例:

class Person
{
public:
  void Print(){
    cout << "name:" << _name << endl;
    cout << "age:" << _age << endl;
    cout << "ID:" << _ID << endl;
  }
public:
  string _name = "張三";

protected:
  int _age = 18;

private:
  int _ID = 1;
};

class Student : public      
[ C++ ] 繼承
[ C++ ] 繼承
[ C++ ] 繼承
[ C++ ] 繼承
[ C++ ] 繼承

2.基類和派生類對象指派轉換

  • 派生類對象可以指派類基類對象/基類的指針/基類的引用。這裡有個形象的說法叫做切片或者切割。寓意把派生類中父類那部分切來指派過去。
  • 基類對象不能指派給派生類對象。
  • 基類的指針或者引用可以通過強制類型轉換指派給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。
[ C++ ] 繼承
[ C++ ] 繼承
class Person
{

//protected:
public:
  string _name; // 姓名
  string _sex;  // 性别
  int  _age;   // 年齡
};

class Student : public Person
{
public:
  int _No; // 學号      
[ C++ ] 繼承

1.子類對象給父類 對象/指針/引用 -- 文法天然支援,沒有類型轉換 

//對象
  Person& rp = s;    //引用
  Person* ptrp = &s;  //指針      
[ C++ ] 繼承

2.基類對象不能指派給派生類對象 

[ C++ ] 繼承
[ C++ ] 繼承

3. 基類的指針可以通過強制類型轉換指派給派生類的指針

//這種情況下轉換是可以的
  ps1->_No = 10;

  ptrp = &p;
  Student* ps2 = (Student*)ptrp;//這種情況轉換時雖然可以,但是會存在越界通路的問題
  ps2->_No = 10;      
[ C++ ] 繼承

3. 繼承中的作用域

  1. 在繼承體系中基類和派生類都有獨立的作用域。
  2. 子類和父類中有同名成員,子類成員将屏蔽父類對同名成員的直接通路,這種情況叫隐藏,也叫重定義。(在子類成員函數中,可以使用 基類::基類成員 顯示通路)
  3. 需要注意的是如果是成員函數的隐藏,隻需要函數名相同就構成隐藏。
  4. 注意在實際中在繼承體系裡面最好不要定義同名的成員。

1. 成員變量導緻隐藏關系

        Student的_num和Person的_num構成隐藏關系,可以看出這樣的代碼雖然能跑,但是非常容易混淆,是以盡量避免重名。

class Person
{
protected:
  string _name = "張三"; // 姓名
  int _num = 111;      // 身份證号
};

class Student : public Person
{
public:
  void Print(){
    cout << " 姓名:" << _name << endl;
    cout << " 學号:" << _num << endl;//會使用自己的成員變量 999
    cout << " 身份證号:" << Person::_num << endl; //要指定作用域
  }
protected:
  int _num = 999; // 學号
};
int main(){
  Student s;
  s.Print();
  return 0;
}      
[ C++ ] 繼承

2. 成員方法導緻隐藏關系

        這段代碼中b的fun()和A的fun()構成隐藏關系,b的fun()隐藏了A的fun(),若要使用A的fun()顯示指定作用域即可通路到A的fun()。

class A
{
public:
  void fun(){
    cout << "A::func()" << endl;
  }
};
class B : public A
{
public:
  void fun(){
    cout << "B::func()"<< endl;
  }
};

int main(){
  B b;
  b.fun();//b的fun()隐藏了A的fun()
  b.A::fun();//指定作用域即可通路到A的func()
  return 0;
};      
[ C++ ] 繼承

4.派生類的預設成員函數

我們在學習類與對象第一節時就知道了類内會預設生成6個成員函數,那麼在派生類中,這幾個成員函數是如何生成的呢?

  1. 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有預設 的構造函數,則必須在派生類構造函數的初始化清單階段顯示調用。
  2. 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
  3. 派生類的operator=必須要調用基類的operator=完成基類的複制。
  4. 派生類的析構函數會在被調用完成後自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
  5. 派生類對象初始化先調用基類構造再調派生類構造。
  6. 派生類對象析構清理先調用派生類析構再調基類的析構。
  7. 因為後續一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同。那麼編譯器會對析構函數名進行特殊處理,處理成destrutor(),是以父類析構函數不加 virtual的情況下,子類析構函數和父類析構函數構成隐藏關系。
[ C++ ] 繼承
[ C++ ] 繼承
class Person
{
public:
  Person(const char* name)
    :_name(name)
  {
    cout << "Person(const char* name)" << endl;
  }
  //拷貝構造
  Person(const Person& p)
    :_name(p._name)
  {
    cout << "Person(const Person& p)" << endl;
  }
  
  //指派拷貝
  Person& operator=(const Person& p)
  {
    cout << "Person& operator=(const Person& p)" << endl;
    if (this != &p)
      _name = p._name;

    return *this;
  }

  ~Person()
  {
    cout << "~Person()" << endl;
  }

protected:
  string _name;//姓名
};

class Student : public Person
{
public:
  Student(const char* name = "", int num = 0)
    :_num(num)
    ,Person(name)
  {
    cout << "Student(const char* name = "", int num = 0)" << endl;
  }

  //拷貝構造
  Student(const Student& s)
    :Person(s)
    ,_num(s._num)
  {
    cout << "Student(const Student& s)" << endl;
  }

  //指派構造
  // s1 = s3
  Student& operator=(const Student& s)
  {
    if (this != &s)
    {
      Person::operator=(s);
      _num = s._num;
    }
    cout << "Student& operator=(const Student& s)" << endl;

    return *this;
  }

  //父類和子類析構函數構成隐藏關系
  //原因:多态的需要,析構函數名同意會被處理成destructor()
  //為了保證析構的順序,先子後父
  //子類析構函數完成後會自動調用父類析構函數,是以不需要我們顯示調用
  ~Student()
  {
    //父類的析構函數我們不需要顯示調用
    //Person::~Person();
    cout << "~Student()" << endl;
  }
protected:
  int _num; //學号      
[ C++ ] 繼承
[ C++ ] 繼承
[ C++ ] 繼承

5. 繼承與友元

友元關系不能繼承,也就是說基類友元不能通路子類私有和保護成員

如果要在Student類内通路Person的友元函數,必須在Student類内提供友元函數

class Student;
class Person
{
public:
  friend void Display(const Person& p, const;
protected:
  string _name; // 姓名
};

class Student : public Person
{
  friend void Display(const Person& p, const;
protected:
  int _stuNum; // 學号
};

void Display(const Person& p, const{
  cout << p._name << endl;
  //要想通路到Student中的保護成員,要提供友元函數      
[ C++ ] 繼承

6. 繼承與靜态成員

基類定義了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; // 研究科目
};
//
int main(){
  Student s1;
  Student s2;
  Student s3;
  Graduate s4;
  Person s;

  cout << " 人數 :" << Person::_count << endl;
  cout << " 人數 :" << Student::_count << endl;
  cout << " 人數 :" << s4._count << endl;

  //同一個位址
  cout << " 人數 :" << &Person::_count << endl;
  cout << " 人數 :" << &Student::_count << endl;
  cout << " 人數 :" << &s4._count << endl;

  return 0;
}      
[ C++ ] 繼承
[ C++ ] 繼承
[ C++ ] 繼承

結論: 基類定義了static靜态成員,則整個繼承體系裡面隻有一個這樣的成員。無論派生出多少個子類,都隻有一個static成員執行個體 。

7. 菱形繼承,菱形虛拟繼承

單繼承:一個子類隻有一個直接父類

[ C++ ] 繼承
[ C++ ] 繼承

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

[ C++ ] 繼承
[ C++ ] 繼承

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

[ C++ ] 繼承
[ C++ ] 繼承

7.1 菱形繼承的問題

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

class Person
{
public:
  string _name; // 姓名
  int _a[10000]; //會有多份_a
};
class Student : virtual public Person
{
protected:
  int _num; //學号
};
class Teacher : virtual public Person
{
protected:
  int _id; // 職工編号
};
class Assistant : public Student, public Teacher
{
protected:
  string _majorCourse; // 主修課程      
[ C++ ] 繼承
[ C++ ] 繼承
[ C++ ] 繼承

這樣會有二義性無法明确知道通路的是哪一個,是以需要我們顯示指定通路那個父類的成員可以解決二義性問題,但是資料備援問題無法解決。

7.2 虛拟繼承

虛拟繼承可以解決菱形繼承的二義性和資料備援的問題。如上面的繼承關系,在Student和Teacher的繼承Person時使用虛拟繼承,即可解決問題。需要注意的是,虛拟繼承不要在其他地

方去使用。

class A
{
public:
  int _a;
  //static int _a;
};

//int A::_a = 0;

//class B : public A
class B : virtual public A
{
public:
  int _b;
};

//class C : public A
class C : virtual public A
{
public:
  int _c;
};

class D : public B, public C
{
public:
  int      
[ C++ ] 繼承

8. 總結

  • 多繼承存在菱形繼承,有了菱形繼承就有菱形虛拟繼承,底層實作就很複雜。是以一般不建議設計出多繼承,一定不要設計出菱形繼承。否則在複雜度及性能上都有問題
  • 繼承群組合 ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        1.   public繼承是一種is-a的關系。也就是說每個派生類對象都是一個基類對象。​​​​​​​​​​​​​​                      2.   組合是一種has-a的關系。假設B組合了A,每個B對象中都有一個A對象。
  • 在繼承方式中,基類的内部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。
  • 對象組合是類繼承之外的另一種複用選擇。對象組合要求被組合的對象具有良好定義的接口

    因為對象的内部細節是不可見的。組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于你保持每個類被封裝。

  • 實際盡量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關系就适合繼承那就用繼承,另外要實作多态,也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合。
// Car和BMW Car和Benz構成is-a的關系
class Car {
protected:
  string _colour = "白色"; // 顔色
  string _num = "陝ABIT00"; // 車牌号
};

class BMW : public Car {
public:
  void Drive(){ cout << "好開-操控" << endl; }
};

class Benz : public Car {
public:
  void Drive(){ cout << "好坐-舒适" << endl; }
};

// Tire和Car構成has-a的關系

class Tire {
protected:
  string _brand = "Michelin";  // 品牌
  size_t _size = 17;         // 尺寸
};

class Car {
protected:
  string _colour = "白色"; // 顔色
  string _num = "陝ABIT00"; // 車牌号
  Tire _t; // 輪胎      

繼續閱讀