C++繼承

Koma_Wong發表於2018-06-17
  • Copyright(C)原文來自GitHub賬號,此為副本,僅為學習,如有侵權請聯絡刪除!

C++繼承

在 C++中繼承與多型難點不是很多,本文將把二者通過一文來總結一下。

  • 其中繼承部分分為以下六個部分:繼承和派生、單繼承、多繼承、虛繼承、派生類的建構函式和解構函式、類的賦值相容性,
  • 多型部分將分為以下虛擬函式和純虛擬函式兩部分。
  • 其中在多型中虛擬函式和純虛擬函式本身就是繼承所引申出的特性,所以繼承和派生在同一文總結還是比較妥當的。

首先來看繼承和派生的基本概念。

我們假設在動物園中需要為每個動物建立類,現在有兩種選擇方案,一種是為每個動物建立獨立的類,彼此之間沒有任何聯絡,另一種是建立一個大類,類中涵蓋所有動物的特性。但這兩種方案都有缺陷,前一種是因為割裂了各種動物之間的聯絡,雖然動物不同,但他們大部分特性還是相同的,這種方式的定義顯然是冗餘了。另一種則是過於臃腫,難以體現彼此之間的差異性。所以比較好的組織方式就是通過樹狀繼承的方式來定義,基類中儲存的是共性部分,派生類繼承基類的資料和操作並在此基礎上增加自己特有的個性的東西,這樣就能很好的解決上面兩個問題。

派生類繼承自基類,基類派生出派生類。派生類會繼承基類的所有屬性和行為,並可以在此基礎上增加新的屬性與行為,但是刪減基類的內容卻是不被許可的。

有了繼承和派生的基本概念,接下來需要了解最簡單也是最常用的單繼承。

在派生類派生自基類時候需要增加派生類保留字,保留字也分為 publicprotectedprivate 等,這裡的保留字將會和基類中的保留字共同得到新的許可權,根據組合一共有九種。如果是公有繼承(即 public 繼承),則在派生類中對基類成員的訪問許可權依舊和基類中相同,即可以訪問基類中被 public 和 protected 修飾的變數或函式。如果是保護繼承(protected 繼承),則原先基類中的 public 和 protected 修飾的變數或函式在派生類中全部變為 protected 型別(即只能在派生類中被訪問,外部不能訪問),原本 private 型別的資料或函式依舊是 private 型別。如果是私有繼承(private 繼承),則無論原本是什麼型別的,在派生類中都會變為private 型別。 雖然上面有九種資料訪問許可權的組合,但通常情況下最常用的依舊是公有繼承,並且如果基類中某個資料經常被派生類訪問,則定義為protected 更加方便(如果資料被定義為 private 型別,則每次操作都需要呼叫基類的函式)。

下面通過一個簡單的例子來解釋類的單繼承。

// 基類|Point 
class Point
{
public:
    Point( int x = 0, int y = 0 ) 
        : _x(x), _y(y) { } 
    Point( const Point& pt ) 
        : _x(pt._x), _y(pt._y) { } 
        
    int GetX() const   {return _x;}
    void SetX( int x ) {_x = x;	} 
    int GetY() const   {return _y;} 
    void SetY( int y ) {_y = y;	}
    
    friend ostream& operator<<( ostream& os, const Point& pt ); 

protected:
    int _x, _y;
};

// 派生類 Point3D
class Point3D: public Point
{

public:
    Point3D( int x = 0, int y = 0, int z = 0 ) 
        : Point(x,y), _z(z) { }

    Point3D( const Point3D& pt3d ) 
        : Point(pt3d._x, pt3d._y), _z(pt3d._z) { } 
        
    int GetZ() const   {return _z;}
    void SetZ( int z ) {_z = z;}

    friend ostream& operator<<( ostream& os, const Point3D& pt3d ); 

protected:
    int _z;
};

在 Point 基類中定義了二維空間的點座標(_x,_y), 現在通過共有派生繼承基類的內容,並增加_z 的值形成三維座標。因為是公有派生,所以在派生類中對基類資料和函式的訪問許可權和基類中原本定義相同。因為派生類中經常需要對_x 和_y 兩個座標進行操作,所以如果在基類中將其定義成 private 型別,派生類將不能直接訪問基類中的元素,所有訪問必須通過基類定義的函式才行。但是這裡定義為 protected 之後,派生類就可以知己對基類中的資料進行操作了,操作更加簡單。 繼承的時候,如果基類函式和派生類具有同名函式,則有可能發生函式覆蓋,呼叫時候會發生二義性。比如下例,基類和派生類中均定義了 Print 函式,當使用基類物件呼叫 Print 函式時會呼叫基類的該函式,而使用派生類物件呼叫 Print 函式時,雖然在派生類中繼承了屬於基類的 Print 同名函式,但只會呼叫派生類中重新定義的 Print 函式。這裡並不是基類中的函式不存在,而是他被派生類中的同名函式覆蓋了,所以如果派生類物件希望訪問基類中被“覆蓋”的函式時,只需要通過名解析規則在函式前加上該函式所屬類名即可。

// 定義同名函式和物件
class Point {	
    void Print();
};

class Point3D: public Point 
{ 
    void Print();
}; 

Point pt( 1, 2 );
Point3D pt3d( 1, 2, 3 );

// 基類物件和派生類物件分別呼叫同名函式
pt.Print();	  // 呼叫 Point 類的 Print 成員函式
pt3d.Print(); // 呼叫 Point3D 類的 Print 成員函式

pt3d.Point::Print()// 名解析規則

多繼承

雖然單繼承模式已經幾乎可以解決所有問題,但 C++本身還提供了多繼承機制,即一個派生類可以從多個基類繼承,多重繼承和但繼承一樣,派生類會繼承所有基類的屬性和操作。但是多繼承會導致一個很麻煩的問題:一個派生類通過多繼承會儲存多個來自基類重複的副本。下面的例子就是這樣的情況:

// 儲存了多副本的情況
class A { … };
class B: public A { … };
class C: public A, protected B { … };

在繼承基類時,會在派生類中完全複製基類的儲存空間,如上的定義中,類 B 中儲存了類 A 的副本,但是在定義類 C 時公有繼承 A,再保護繼承類 B 就會導致類 A 的副本被複制了兩次(一次來自自身繼承,一次來源於類 B)。另外,在多繼承情況下,同名函式的呼叫更加複雜,如果要準確呼叫想要的函式需要最好還是完整給出名解析(自底向上回溯)。繼承中會有儲存多副本的問題,所以就引入了虛基類來解決這個問題。我們可以通過下面的例子來學習虛繼承:

// 虛繼承
class A  {public: void f();};
class B: virtual public A {public: void f();}; 
class C: virtual public A {public: void f();}; 
class D: public B, public C {public: void f();};

虛繼承的目的就是為了消除派生類公共基類的對個副本,只儲存一份副本。在派生時使用關鍵字 virtual,上例中類 B、類 C 均繼承基類 A,之後在定義類 D 時候,繼承 B 時已經包含了 A,則在 C 中的 A 就不會再被複制到類 D 中,D 中只含有類 A 的一份副本。雖然在單繼承中也可以在繼承時加上virtual 關鍵字,但虛基類的概念實際上只有多繼承時才有意義。

最後討論的是繼承中最基本問題:建構函式和解構函式的呼叫順序。

建構函式的執行順序為:

  • 1.呼叫基類建構函式,呼叫順序和基類在派生類中繼承順序相同;
  • 2.呼叫派生類中物件成員的建構函式,構造順序和派生類中定義順序相同;
  • 3.呼叫派生類建構函式。

解構函式執行順序為:

  • 1.呼叫派生類建構函式;
  • 2.呼叫你派生類新增物件成員解構函式,析構順序和定義順序相反;
  • 3.呼叫基類建構函式,呼叫順序和基類在派生類中繼承順序相反。

在物件導向程式設計中,我們構建了一個類的層次,在使用時候往往需要使用指向基類的指標或基類的引用,這些都是類的賦值相容性問題,具有以下情形:

  • 1.將派生類物件賦值給基類物件,僅賦值基類部分;
  • 2.用派生類物件初始化引用物件,僅操作基類部分;
  • 3.使指向基類的指標指向派生類物件,僅引領基類部分。下面通過一個例子來詳細說明賦值相容性質問題:
#include <iostream>
#include <string> 

using namespace std;

// 定義類及派生類
class Base
{
public:
    Base(string s) 
        : str_a(s) { }
    Base(const Base & that) { str_a = that.str_a; }
    
    void Print() const { cout << "In base: " << str_a << endl; } 
    
protected:
    string str_a;
};

class Derived : public Base
{
public:
    Derived(string s1,string s2) 
        : Base(s1), str_b(s2) { }	// 呼叫基類建構函式初始化
    void Print() const { cout << "In derived: " 
                              << str_a + " " + str_b 
                              << endl; } 
protected:
    string str_b;
};

//  主函式
int main()
{
    Derived d1( "Hello", "World" );
    Base b1( d1 );	// 拷貝構造,派生類至基類,僅複製基類部分
    d1.Print();	// Hello World

    b1.Print();	// Hello
    Base & b2 = d1;	// 引用,不呼叫拷貝建構函式,僅訪問基類部分
    d1.Print();

    b2.Print();

    Base * b3 = &d1;// 指標,不呼叫拷貝建構函式,僅引領基類部分

    d1.Print();
    b3->Print(); 
    return 1;
}

通過上面賦值相容性的例子可以看出,當我們使用一個基類的指標指向派生類物件時,只能操作定義在基類中的函式,而不能呼叫派生類中的函式,哪怕是同名函式也不行。為了能夠僅使用指向基類的指標,並通過取得不同物件地址使收到相同資訊時做出不同響應,引入了虛擬函式的概念,並進一步引出了純虛擬函式和抽象類的概念,具體總結可以見我之前的文章。

相關文章