Lab 13-1: 異質容器, 虛擬函式與多型

 
實習目標 練習設計並且運用異質容器
設計簡單的繼承架構
配合異質容器練習使用 C++ 多型的特性
 
步驟一 在這次的實習中, 我們要繼續擴充 StudentList 實習, StudentListIterator 實習, 以及 LoggedStudentList 實習, 希望在很少量的程式更動下, 擴充上面三個類別儲存 Student 物件的功能, 讓它們也可以儲存 UndergraduateStudent 型態的物件, GraduateStudent 型態的物件, 以及 ForeignGraduateStudent 型態的物件。
步驟二 首先, 先回憶一下 Student 類別的定義
    class Student
    {
    public:
        Student(const char *name, const char *ID, 
                const char *phone, const char *department);
        ~Student();
        void display(ostream &os) const;
        bool IDEquals(const char *id) const;
        bool ofTheSameDepartment(Student &student2) const;
    private:
        char *m_name;
        char *m_ID;
        char *m_phone;
        char *m_department;
    };
這個類別有一個建構元, 一個解構元, 三個成員函式: display(), IDEquals() 以及 ofTheSameDepartment(), 以及一些基本的學生資料

在嘗試對 Student 類別進行修改, 以及設計繼承的架構之前, 我們可以先來看一下預期的使用方法

    #include <iostream>
    #include <fstream>
    using namespace std;
    
    #include "UndergraduateStudent.h"
    #include "GraduateStudent.h"
    #include "ForeignGraduateStudent.h"
    #include "LoggedStudentList.h"
    #include "StudentListIterator.h"
    
    void main()
    {
        ofstream logfile("main2.log");
        LoggedStudentList sList(logfile);
        sList.appendEntry(new UndergraduateStudent("Mary Chen", "111111111", 
                                      "0933111111", 
                                      "Business",
                                      "John Viega"));
        sList.appendEntry(new UndergraduateStudent("John Wang", "222222222", 
                                      "0928222222", 
                                      "Computer Science",
                                      "Dan Smart"));
        sList.appendEntry(new GraduateStudent("Mel Lee", "333333333", 
                                      "0968333333", 
                                      "Mechanical Engineering",
                                      "Ron Rivest"));
        sList.appendEntry(new GraduateStudent("Bob Tsai", "444444444", 
                                      "0930444444", 
                                      "Electrical Engineering",
                                      "Alan Laub"));
        sList.appendEntry(new ForeignGraduateStudent("Ron Yang", "555555555", 
                                      "0918555555", 
                                      "Computer Science",
                                      "Allen Gersho",
                                      "Singapore"));
    
        int i;
        StudentListIterator iter1(sList), iter2(sList);
        for (i=0, iter1.reset(); 
             iter1.hasMoreData(); iter1.next(), i++)
        {
            cout << i << ":";
            iter1->display(cout);
            cout << endl;
        }
    
        sList.find("222222222")->display(cout); cout << " found!!" << endl;
    
        if (sList.deleteEntry("444444444"))
            cout << "Bob Tsai's entry deleted successfully!\n";
        else
            cout << "Bob Tsai's entry deletion failed!\n";
    
        if (sList.find("444444444") == 0)
            cout << "Can not find Bob Tsai's entry!\n";
    
    
        cout << endl;
        for (iter1.reset(); iter1.hasMoreData(); iter1.next())
        {
            for (iter2=iter1, iter2.next(); 
                 iter2.hasMoreData(); iter2.next())
            {
                if (iter1->ofTheSameDepartment(*iter2))
                {
                    cout << "The following two students"
                            " are of the same department:\n";
                    iter1->display(cout);
                    cout << endl;
                    iter2->display(cout);
                    cout << endl;
                }
            }
        }
    
        cout << endl;
        for (iter1.reset(); iter1.hasMoreData(); iter1.next())
            if (iter1->IDEquals("333333333"))
                sList.insertEntry(iter1, 
                new UndergraduateStudent("Carol Chen", "333331111", 
                                         "0933333111", "Business",
                                         "John Fowler"));
        for (i=0; i<sList.size(); i++)
        {
            sList[i]->display(cout);
            cout << endl;
        }
    
        sList.dump();
    }
上面程式除了紅字部份之外, 和實習 11-1 的步驟三的程式幾乎是相同的, 我們該如何修改 Student 類別, 並且設計 UndergraduateStudent, GraduateStudent 以及 ForeignGraduateStudent 這三個類別來符合上面這一段程式的要求?

請注意, 上面程式中藍字部份的程式並沒有修改, 標示為藍色的意思是提醒你這些函式的呼叫會產生動態繫結 (dynamic binding) 的需求。

步驟三 由上面的測試程式中分析一下程式的需求如下:

  1. 在 LoggedStudentList 或是 StudentList 中除了可以運用 appendEntry() 加入 Student 類別的物件之外, 需要可以處理 UndergraduateStudent, GraduateStudent 以及 ForeignGraduateStudent 的物件, StudentListIterator 需要能夠正常運作

  2. display() 界面需要能夠透過多型指標來操作

  3. 解構元 dtor 需要能夠透過多型指標來操作
一種最直接的方式是定義三個獨立的類別: UndergraduateStudent, GraduateStudent, ForeignGraduateStudent, 然後替 StudentList 類別定義一組 overloaded appendEntry() 和 insertEntry() 的界面函式, 例如:
    void appendEntry(UndergraduateStudent *student);
    void insertEntry(StudentListIterator iter, UndergraduateStudent *student);
    void appendEntry(GraduateStudent *student);
    void insertEntry(StudentListIterator iter, GraduateStudent *student);
    void appendEntry(ForeignGraduateStudent *student);
    void insertEntry(StudentListIterator iter, ForeignGraduateStudent *student);
但是這樣子還是沒有完全解決問題, 在 StudentList::Node 類別內我們原先運用 Student *m_data 來記錄每一個物件, 如此只能記錄 Student 類別的物件, 其它三種類別的物件必須另外想辦法處理

還好在這個實習中我們需要處理的三種額外的類別的本質其實都是學生, 所以我們可以運用繼承的語法來設計適當的類別階層, 並且讓 StudentList 成為一個異質的陣列

步驟四 首先根據上面運用方法推敲以及對於 UndergraduateStudent, GraduateStudent, ForeignGraduateGraduate 的基本認識, 這三個類別除了應該要有原來 Student 類別的基本屬性外, 主要需要增加的屬性如下:
  • UndergraduateStudent: m_academicAdvisor (指導老師)
  • GraduateStudent: m_advisor (論文指導老師)
  • ForeignGraduateStudent: m_advisor (論文指導老師), m_nationality (國籍)
我們可以運用 C++ 中繼承的語法實作下面的類別階層
以 UndergraduateStudent 類別為例: 類別的宣告如下:
    class UndergraduateStudent: public Student
    {
    public:
        UndergraduateStudent(const char *name, const char *ID, 
                             const char *phone, const char *department,
                             const char *academicAdvisor);
        ~UndergraduateStudent();
        void display(ostream &os) const;
    private:
        char *m_academicAdvisor;
    };
建構元中必須初始化 m_academicAdvisor 資料成員, 同時因為需要配置記憶體, 所以也就需要在解構元中釋放記憶體

建構元也需要在初始化串列中運用父類別的建構元函式初始化父類別子物件

因為 UndergraduateStudent 類別的物件需要放進 StudentList 中, 需要以多型指標來處理, 所以解構元必須要是虛擬函式。 要宣告一個虛擬函式並不是在這個類別中函式的定義前加上 virtual 就夠了, 必須在你想要用的多型指標的那個類別中宣告, 因為我們要在 StudentList::Node 中用 Student *m_data 這個多型指標來處理所有的資料, 所以必須宣告 Student::~Student() 這個解構元為 virtual

另外為了讓 void UndergraduateStudent::display(ostream &os) const; 可以運用同樣的多型指標來操作, 我們也修改 void Student::display(ostream &os) const 函式為 virtual

做到這裡你可以先修改一下步驟二中的 main() 函式, 暫時先不用 GraduateStudent 和 ForeignGraduateStudent 類別, 先用 StudentList 而不用 LoggedStudentList, 測試結果如下

0:[Mary Chen, 111111111, 0933111111, Business] [AcademicAdvisor:John Viega]
1:[John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart] 
2:[Mel Lee, 333333333, 0968333333, Mechanical Engineering] [AcademicAdvisor:Ron Rivest] 
3:[Bob Tsai, 444444444, 0930444444, Electrical Engineering] [AcademicAdvisor:Alan Laub] 
4:[Ron Yang, 555555555, 0918555555, Computer Science] [AcademicAdvisor:Allen Gersho] 
[John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart] found!!
Bob Tsai's entry deleted successfully!
Can not find Bob Tsai's entry!
...
範例執行程式
步驟五 接下來請依照上一步驟中類別的繼承圖, 製作 GraduateStudent 類別和 ForeignGraduateStudent 類別

使用步驟二中的 main() 函式, 將 LoggedStudentList 改為 StudentList 測試一下, 結果如下:

0:[Mary Chen, 111111111, 0933111111, Business] [AcademicAdvisor:John Viega]
1:[John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart]
2:[Mel Lee, 333333333, 0968333333, Mechanical Engineering] [Advisor:Ron Rivest]
3:[Bob Tsai, 444444444, 0930444444, Electrical Engineering] [Advisor:Alan Laub]
4:[Ron Yang, 555555555, 0918555555, Computer Science] [Advisor:Allen Gersho] 
                                          [Nationality:Singapore]
[John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart] found!!    
Bob Tsai's entry deleted successfully!
Can not find Bob Tsai's entry!
...
範例執行程式
步驟六 接下來我們應該要測試 LoggedStudentList, 看看它和 Student, UndergraduateStudent, GraduateStudent 和 ForeignGraduateStudent 這幾個類別合作的狀況如何

使用步驟二中的 main() 函式測試一下, 結果如下:

0:[Mary Chen, 111111111, 0933111111, Business] [AcademicAdvisor:John Viega]
1:[John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart]
2:[Mel Lee, 333333333, 0968333333, Mechanical Engineering] [Advisor:Ron Rivest]
3:[Bob Tsai, 444444444, 0930444444, Electrical Engineering] [Advisor:Alan Laub]
4:[Ron Yang, 555555555, 0918555555, Computer Science] [Advisor:Allen Gersho] 
                                          [Nationality:Singapore]
[John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart] found!!    
Bob Tsai's entry deleted successfully!
Can not find Bob Tsai's entry!
...
範例執行程式

請檢查所產生的 main2.log 檔案內的記錄資料

LoggedStudentList::appendEntry()
   [Mary Chen, 111111111, 0933111111, Business] [AcademicAdvisor:John Viega]
LoggedStudentList::appendEntry()
   [John Wang, 222222222, 0928222222, Computer Science] [AcademicAdvisor:Dan Smart]
LoggedStudentList::appendEntry()
   [Mel Lee, 333333333, 0968333333, Mechanical Engineering] [Advisor:Ron Rivest]
LoggedStudentList::appendEntry()
   [Bob Tsai, 444444444, 0930444444, Electrical Engineering] [Advisor:Alan Laub]
...
步驟七 如果希望將 Student 類別設計成一個 Abstract Class, 不讓使用者在程式中產生 Student 類別的物件, 我們應該在 Student 類別中挑選至少一個函式作為純粹虛擬函式, 例如 Student::display(ostream&):
    virtual void display(std::ostream &os) const=0;
同時需要將原來程式中使用 Student 物件的地方以其它方式取代 , 如果你先前有使用 Student Dummy 物件的話, 也需要用其他的物件取代, 例如 UndergraduateStudent。
步驟八 請助教檢查後, 將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來, 選擇 Lab13-1 上傳, 後面的實習課程可能需要使用這裡所完成的程式

多型呼叫語法測試

C++ 物件導向程式設計課程 首頁

製作日期: 05/25/2004 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程系 Lagoon