2011年12月5日 星期一

類別的繼承與指標應用

甚麼是繼承(inherit)?
C++繼承是一種抽象的概念,有點將軟體元件(類別)擬人化的味道,先就字面意義來說,繼承是一種父子關係,宣告子類別指定繼承自某父類別意即子類別擁有父類別的特徵,在 C++ 語言中的具體表現是子類別可以使用父類別的所有 public 與 protected 資料成員與函式成員。
繼承的另一層面意義是「世襲」,子類別可以世襲父類別的位置意即子類別可以在系統中扮演父類別的角色,在 C++ 語言中的具體表現是父類別的指標可以指向子類別的 instance,易言之,在一套軟體當中,經過前面幾篇的介紹,你可以想像得到以 C++ 撰寫的軟體系統,經常設計了許許多多的類別,然後宣告或 new 出這些類別的 instance 來使用、串連、傳遞。例如前一篇的 Grade Class  範例當中我們設計了 GradeList 類別來提供班級的成績儲存與計算,並在 UserApplication.app 中使用它的 instance 來裝載資料,由於 C 語言的繼承特性,就算我們在設計這套系統的時候並不知道日後會有哪些新類別的存在,後來開發的新類別只要繼承自原來既有的類別,它們的 instance 也能夠在系統上被使用,舊系統不用改一行程式碼,例如本篇的 Grade Sheet 範例,我們設計了繼承自 GradeList 類別的 GradeSheet 類別,那麼它的 instance 也能夠在原來的 UserApplication.app 使用。
繼承的語法僅是在設計新類別的時候,在新的子類別名稱後面以 : 號指定以甚麼樣的權限繼承自哪個父類別,如下所示,父類別也稱為子類別的基礎類別(base class),子類別也稱為父類別的衍生類別(derived class)。
class 子類別名稱 : 繼承權限 父類別名稱
{
    ...子類別宣告...
};

繼承權限也分為 public 或protected 或 private 三種,它們會決定父類別中的資料與函式成員在這個子類別中的授權程度,最常見 public 繼承就是父類別的成員在子類別中都還是維持原權限,而 private 繼承則讓父類別的成員不能再讓被子類別的子類別繼承,以下是一個 public 繼承的例子,左邊的 GradeList 類別是前篇的範例,內容是一個班級的教師名稱、學生成績(動態陣列)、以及學生人數,右邊的 GradeSheet 類別繼承自 GradeList,所以 GradeSheet 類別已經擁有了 GradeList 所有的資料和函式成員,它只有再增加一個 index 代表班級的編號。
GradeList.h GradeSheet.h
class GradeList
{
   public:
   struct GRADE
    {   int id; 
        float value;  };

   char teacher[16]; 
   GRADE * grade; 
   int students;  

   ...函式成員宣告...
};     
class GradeSheet : public GradeList
{
    public:
    int index;

   ...函式成員宣告...
};      
在這個例子當中,讓我們觀察父子類別 GradeList 與 GradeSheet 在記憶體中的儲存狀況,你可以看到,由於 GradeSheet 繼承自 GradeList 類別,所以編譯器直接給了它一份父類別 GradeList 的記憶體布局(memory layout),然後把子類別新增加的 index 附加在後面,無怪乎父類別 GradeList * 指標也可以用來存取子類別的 instance。

註: 這個簡單的例子是因為 GradeList 與 GradeSheet 都沒有虛擬函式(virtual function),而且 GradeSheet 乃是單一繼承的子類別,記憶體才會如此單純,有虛擬函式或多重繼承(一個子類別有兩個以上的父類別)的記憶體布局較為複雜,但原理大同小異。
從這個例子我們可以知道類別繼承的效果之一就是子類別的結構包含父類別的結構,所以父類別的指標可以指向子類別的 instance。上面的例子是公開繼承,所以 GradeSheet 原封不動地接收了 GradeList 的資料與函式成員,也就是 GradeSheet 的 instance 完全可以被當作是一個 GradeList 的 instance 來使用,其他繼承權限的效果如下所述,可以營造出不同的效果。

公開繼承 (例如 class GradeSheet : public GradeList)

子類別繼承的資料與函式成員保持父類別的權限,public 還是 public, protected 還是 protected。

保護繼承 (例如 class GradeSheet : protected GradeList)

子類別繼承的資料與函式成員都變成保護的成員,public 變成 protected, protected 還是 protected。

私有繼承 (例如 class GradeSheet : private GradeList)

子類別繼承的資料與函式成員都變成私有的成員,public 變成 private, protected 也變成 private,讓父類別的成員無法再被繼承。
子類別的建構與解構
前篇提 到,建構函式和解構函式是類別的 instance 會自動執行的初始化與清理行為,當類別之間有繼承的父子關係的時候,理應執行父子類別的建構函式和解構函式,以保證不論是父類別或子類別的資料成員都會正 確地被初始化或清理,然而執行的順序會攸關這個行為的正確與否,還好編譯器會自動地做好了完善的安排。讓我們看下面這個實驗例子,當中父子類別的建構函式 和解構函式都包含有 printf( ) 述句,所以我們可以藉由觀察執行結果得知它們執行的順序。
GradeList::GradeList()
{  printf("GradeList::GradeList()\n");  }

GradeList::~GradeList()
{  printf("GradeList::~GradeList()\n");  }

GradeSheet::GradeSheet()
{  printf("GradeSheet::GradeSheet()\n");  }

GradeSheet::~GradeSheet()
{  printf("GradeSheet::~GradeSheet()\n");  }

void main()
{  GradeSheet sheet;  }
執行結果是:
GradeList::GradeList()
GradeSheet::GradeSheet()
GradeSheet::~GradeSheet()
GradeList::~GradeList()

所以我們可以得到結論如下:

建構順序是 父→子

先執行父類別的建構函式,再執行子類別的建構函式。

解構順序是 子→父

先執行子類別的解構函式,再執行父類別的解構函式。
道理很簡單,子類別建構函式當中可能去使用父類別的資料成員,甚至更改它的初始值,所以父類別的建構函式理應先執行,才能夠準備好以初始化的資料成員讓子 類別使用。而解構函式則相反,因為子類別在解構函式當中仍可能去使用父類別的資料成員,所以在那當下還不能夠清理掉父類別的資料成員,所以子類別的解構函 式理應先執行,最後才執行父類別的解構函式。
is a 與 has a 的概念
這是在 C++ 的教學與探討書籍中經常會宣揚的觀念,繼承是一種 is-a 關係(子類別是一種父類別),所以子類別有兩個重要的性質: 1. 子類別也具有父類別的特徵(意指資料與函式成員),2. 子類別也可以被當作父類別使用。類別之間的另一種相比較的關係是 has-a 關係,某個 A 類別 has a B  類別說穿了只不過是 A 類別的資料成員裡面有一個 B 的 instance 罷了,你可以把 A 稱為 B 的容器類別,如下面例子所示。
GradeSheet is a GradeList Course has a GradeList
class GradeList
{
    public:
    char teacher[16]; 
    GRADE * grade; 
    int students; 

    ...函式成員宣告...
};

class GradeSheet : public GradeList
{
    public:
    int index;

    ...函式成員宣告...
};     
class GradeList
{
    public:
    char teacher[16]; 
    GRADE * grade; 
    int students; 

    ...函式成員宣告...
};

class Course
{
   public:
   char name[16];
   GradeList list;

   ...函式成員宣告...
};      
在這個例子當中,GradeSheet is a GradeList 的意義是成績報表(grade sheet)也是一種成績清單(grade list),在實務上的意義是 GradeSheet 承襲了 GradeList的記憶體佈局,而 Course has a GradeList 的意義是課程資料(Course)包括了成績清單(grade list),在實務上的意義是 Course 包括了 GradeList的資料,兩者有甚麼差異呢?首先請看 GradeList、GradeSheet、Course 這三者的記憶體布局,由於GradeSheet 繼承自 GradeList,所以它自己的 index 資料成員附加在 GradeList 的資料成員之後,而 Course 類別同樣也包含了 GradeList 的資料,但它在記憶體中的位置則依宣告順序而定。

接下來看建構函式和解構函式的執行順序,我們同樣以一段程式碼做實驗:
GradeList::GradeList()
{  printf("GradeList::GradeList()\n");  }

GradeList::~GradeList()
{  printf("GradeList::~GradeList()\n");  }

Course::Course()
{  printf("Course::Course()\n");  }

Course::~Course()
{  printf("Course::~Course()\n");  }

void main()
{  Course course; }
執行結果如下:
GradeList::GradeList()
Course::Course()
Course::~Course()
GradeList::~GradeList()
結果是 has-a 這種關係,容器類別建構函式當中可能去使用資料成員,所以成員的建構函式理應先執行,才能夠準備好以初始化的資料成員讓容器類別使用。而解構函式則相反,因為容器類別在解構函式當中仍可能去使用資料成員,所以容器類別的解構函式理應先執行,最後才執行成員的解構函式。

建構順序是: 成員→容器

先執行成員類別的建構函式,再執行容器類別的建構函式。

解構順序是: 容器→成員

先執行容器類別的解構函式,再執行成員類別的解構函式。
使用父類別的指標
前面一再提到類別繼承的意義在於,子類別也具有父類別的特徵(意指資料與函式成員),以及子類別也可以被當作父類別使用,例子如下,子類別可使用父類別的資料與函式:
void main(void)
{
    // 宣告一個GradeSheet 類別的instance:
    GradeSheet sheet;

    // 使用 GradeList 和 GradeSheet 的資料成員:
    printf(" School: %s\n", GradeList::school);
    sheet.index = 5;
    printf(" Sheet index: %d\n", sheet.index);

    // GradeSheet 使用 GradeList 的函式成員:
    sheet.adjust_grade_list(); 
    sheet.sort_grade_list();
}
在這個例子當中,你可以看到 main( ) 當中根本就把 GradeSheet 的 instance 當作 GradeList 使用,但若僅僅如此,你仍然得把原程式碼當中宣告使用 GradeList instance 的部分都改為宣告 GradeSheet instance,所以要發揮繼承的優點,你應該盡量用父類別的指標來撰寫程式,如下例:
void main(void)
{
    // 宣告一個 GradeSheet 類別的 instance:
    GradeSheet sheet;

    // 用一個 GradeList 指標去指向這個 instance:
    GradeList * ptr = &sheet;

    // 透過 GradeList * 指標來使用此 instance:
    ptr->adjust_grade_list();
    ptr->sort_grade_list();
}
用父類別的指標來寫程式有甚麼好處?以下面的校務系統為例,在設計的時候,系統的行為完全是由父類別的指標所組成的,所以開發的時候不需要知道衍生類別的設計,新類別自然可參與舊系統,從這個例子你也可以看到,C++ 中的繼承代表了向後的擴充性
// 假設這三個類別都繼承 GradeList:
GradeSheet *pSheet = new GradeSheet();
OtherSheet *pOther = new OtherSheet();
SpecialSheet *pSpecial = new SpecialSheet();
...其他衍生類別的 instance...

// 用父類別指標陣列來裝子類別instances 的指標:
GradeList * pList[10];
pList[0] = pSheet;
pList[1] = pOther;
pList[2] = pSpecial;
pList[3] = ...;

// 假設校務系統的設計是呼叫所有班級的分數調整函式:
for (int i=0; i<10; ++i)
{
    pList[i]->adjust_grade_list();
}
透過 C++ 的繼承語法和父類別指標的使用,設計父類別的時候,不需要知道子類別的設計,但是子類別可以參與父類別的 frame work。設計子類別的時候,可以重複使用父類別的設計,而且透過繼承的方式,設計子類別的時候不需要更動父類別的程式碼,可說是最佳的程式碼 reuse 方式。

資料來源: http://www.csie.nctu.edu.tw/~skyang/inherit.zhtw.htm

1 則留言:

  1. fallout 76 black titanium genesis PCB - The TITanium Arts
    › › genesis › genesis This is a list of all rocket league titanium white the PCB's for the pure titanium earrings genesis PCBs. If race tech titanium you're looking for a 100% complete model of the top titanium paint color model of the universe, we've titanium necklace gathered their

    回覆刪除