1. 程式人生 > >C++PrimerPlus學習之類繼承

C++PrimerPlus學習之類繼承

公有派生

  • 基類的公有成員及私有成員都會成為派生類的一部分
  • 基類的私有成員只能通過基類的公有和保護方法訪問
  • 基類指標或引用可以在不顯式型別轉換的情況下指向派生類物件
  • 派生類的建構函式
    • 首先會建立基類物件,派生類的建構函式應通過成員初始化列表將基類資訊傳遞給基類建構函式
    • 一個需要注意的地方
      Point::Point(int tk,const Base &b):Base(b)
      {
      	k=tk;
      }
      
      由於b的型別為Base &,因此將呼叫基類的複製建構函式,如果基類使用了動態記憶體分配(new)的話,則需要定義基類的複製建構函式。
  • 派生類的解構函式
    • 派生類物件過期時,程式將首先呼叫派生類解構函式,然後再呼叫基類解構函式

繼承的一些關係

  • C++有3種繼承方式:公有繼承,保護繼承,私有繼承
  • is-a(is-a-kind-of)關係:如香蕉是水果。建模方式:那麼香蕉類可以繼承水果類
  • has-a關係:如午餐有水果。建模方式:午餐類裡有水果類這個資料成員
  • is-like-a關係:如律師像鯊魚。建模方式:設計一個包含共有特徵的類,然後以is-ahas-a關係,在這個類的基礎上定義相關的類。(以is-a關係建立有點抽象類的感覺
  • is-implemented-as-a(作為......來實現)
    關係:如使用陣列來實現棧。建模方式:讓棧包含一個私有Array物件。
  • uses-a關係:如計算機可以使用鐳射印表機。建模方式:使用友元函式或類來處理印表機和計算機之間的關係。

多型-動態多型

  • 定義

    • 同一個方法在派生類和基類中的行為是不同的
    • 方法取決於呼叫該方法的物件
    • 前面所學的過載和函式模板是在編譯時間便確定的聯編,稱為靜態多型
  • 重寫基類方法

    • 基類的方法可以在派生類中重寫 – 使用classname::來說明是基類的還是派生類的

    • 可在派生類中使用基類名作為限定符呼叫同名的基類函式

      void Point::show()
      {
      	Base::show();
      	cout<<z<<endl;
      }
      
    • 如果基類中的函式有多個過載,則繼承過來的時候不能只重新定義一個版本的 – 另外的會被隱藏

      
      class Base
      {
      public:
      	void show(int a)const
      	{
      		cout<<a<<endl;
      	}
      	void show()const
      	{
      		cout<<10<<endl;
      	}
      	void show(double a)const
      	{
      		cout<<a<<endl;
      	}
      };
      class P : public Base
      {
      public:
      	void show(int a)const
      	{
      		cout<<a+10<<endl;
      	}
      	/*void show()const
      	{
      		cout<<20<<endl;
      	}
      	void show(double a)const
      	{
      		cout<<a+10<<endl;
      	}*/
      
      };
      int main()
      {
      	P a;
      	a.show();//invalid
      }
      
  • 虛擬函式

    • 宣告前加virtual,定義不用加。

    • 如果方法是通過引用或指標而不是物件呼叫的,程式將根據引用或指標指向的物件的型別來選擇方法。

    • 如果沒有加virtual(一般方法),那麼程式將根據引用或指標的型別來選擇方法。

    • 友元函式不能是虛擬函式,因為友元函式不是類成員,而只有成員函式才能是虛擬函式。

    • 可以建立指向基類的指標陣列,那麼這個陣列既可以指向基類,也可以指向派生類。

      int main() {
      	Base b(10, 20);
      	Point x(1, 2, 3);
      	Base* bs[3];
      	bs[0] = new Base(1, 2);
      	bs[1] = &x;
      	bs[2] = &b;
      	rep(i, 0, 3) bs[i]->show(), cout << endl;
      	return 0;
      }
      /*
      output:
      1 2
      1 2 3
      10 20
      */
      
    • 一個需要注意的地方:基類需要宣告一個虛解構函式,這樣做是為了保證在釋放物件時,可以呼叫相應物件型別的解構函式。

    • 沒有重新定義

      • 重新定義一個不接受引數的show函式,那麼將會隱藏同名基類的方法。
      • 兩條經驗規則
        • 如果重新定義繼承的方法,應確保與原來的原型完全相同。
        • 如果返回型別是基類引用或指標,則可以修改為指向派生類的引用或指標(這種例外是新出現的)。這種特性被稱為返回型別協變。
      class Base
      {
      public:
      	virtual void show(int a)const;
      ...
      };
      class Point
      {
      public:
      	virtual void show()const;
      ...
      };
      Point tmp;
      tmp.show()//valid
      tmp.show(1);//invalid
      

靜態聯編和動態聯編

  • 將原始碼中的函式呼叫解釋為執行特定的函式程式碼塊被稱為函式名聯編
  • 函式過載和函式模板C/C++編譯器可以在編譯過程完成這種聯編。在編譯過程中進行聯編被稱為靜態聯編。
  • 虛擬函式,因為編譯器不知道使用者將選擇哪種型別的物件。所以編譯器必須生成能夠在程式執行時選擇正確的虛擬函式方法,這被稱為動態聯編。
  • C++預設選擇為靜態聯編,因為效率更高。

虛擬函式的實現原理

  • 給每個物件新增一個隱藏成員。隱藏成員中儲存了一個指向函式地址陣列的指標
  • 該地址陣列稱為虛擬函式表
  • 對於基類來說,基類物件包含了一個指標,該指標指向基類中所有虛擬函式的地址表
  • 對於派生類來說,派生類物件也包含了一個指標,該指標指向派生類中所有虛擬函式的地址表
    • 如果派生類沒有重新定義某虛擬函式,則此表保留此虛擬函式基類版本的地址
    • 如果派生類重新定義了某虛擬函式,則此表將更新此虛擬函式的新地址
    • 如果派生類增加了新的虛擬函式,則此表也增加該函式的地址
  • 不管虛擬函式有多少個,都只需要在物件裡新增一個指標成員
  • 呼叫虛擬函式時
    • 程式將檢視儲存在物件中的虛擬函式表頭地址
    • 然後轉身相應的函式地址表
    • 然後根據該虛擬函式在類中宣告的位置找到其在表中的位置
    • 然後跳到該地址指向的函式地址,執行函式
      在這裡插入圖片描述

抽象基類(abc)

  • abstract base class

  • 使用純虛擬函式提供未實現的函式 – 在宣告的結尾處加 =0

    class Base {
    private:
    ...;
    public:
    virtual double get_area() = 0;
    };
    
  • 包含純虛擬函式的類只用作基類 – 不能例項化 – 但是能宣告(但不初始化)指標

  • 即ABC必須至少包含一個純虛擬函式

  • 如果在基類中聲明瞭純虛擬函式,而派生類中並沒有對其定義,則該函式仍為純虛擬函式,派生類也為抽象類

  • 基類中也可以定義(實現)純虛擬函式,但在派生類中必需也要定義並且顯示地呼叫(使用類名限定符)

    • 這樣可以將不同子類中公共的事務放在父類中完成
    • 只有宣告而沒有定義的純虛擬函式派生類是無法呼叫的
    • 如果要把基類的解構函式宣告為純虛擬函式(有時候這麼做只是為了說明此類為抽象類),則必須定義這個純虛解構函式的實現 – 因為派生類析構時會自動呼叫它
  • 純虛擬函式作為一種“介面約定”, 在基於元件的程式設計模式中很常見 (華哥這麼說

    • 派生元件至少會實現基類元件的所有介面(純虛擬函式)

繼承和動態記憶體分配

  • 如果基類使用了動態記憶體分配 – 即在構造中使用new分配空間
    • 該基類需要宣告其建構函式, 解構函式,複製構造,賦值運算子
  • 如果此時子類中沒有使用new分配的記憶體
    • 此子類預設的複製構造會顯式地呼叫基類的複製構造, 同時根據成員變數型別進行復制
    • 此子類預設的賦值運算子會顯式地呼叫基類的賦值運算子
  • 如果子類使用new分配的記憶體
    • 必須為子類定義顯式解構函式

    • 必須為子類定義複製建構函式

      • 基類的複製建構函式中的引數是基類的引用,所以可以傳遞進來派生類物件。
      Point::Point(const Point &a):Base(a)	
      {
      	z=a.z;
      }
      
    • 必須為子類過載賦值運算子

      • 顯式呼叫基類的賦值運算子,以完成基類部分的賦值
      String& String::operator=(const String& s) {    
      if(this == &s) return *this;
      Base::operator=(s);             // 注意此句 -- 必須顯示呼叫基類的賦值運算子
      delete[] str;                           
      len = s.len;
      str = new char[len+1];
      strcpy(str, s.str);
      return *this;
      }
      

繼承與友元函式

// Base.h
class Base {
    ...;
    friend ostream& operator<< (ostream& out, const Base& p) {
        ...;
    } // 基類的友元函式
};

// Point.cpp
ostream& oeprator<< (ostream& out, const Point& p) {
    out << (const Base&) p; // 輸出基類部分
    ...;                    // 輸出派生部分
} // 派生類的友元函式
  • 在派生類的友元函式中, 只能訪問派生類的派生部分,而不能訪問基類的私有成員
  • 可使用基類的友元函式來負責對派生類的基類部分的訪問
  • 需要顯式轉換型別。