Lab 14-1: Generic Programming -
Template of Managed Pointer

 
實習目標 練習撰寫 template class
特別注意 template class 的程式碼應該放在 *.h 檔案中
請觀察撰寫 template class 時所出現的語法錯誤
請觀察有 template class 時的執行檔大小
 
步驟一 在這一個練習中我們要逐步地來觀察為什麼需要撰寫 template class, 並且將一個現成的類別改寫為 template class。 首先, 我們先瞭解下面 Fred 和 FredPtr 類別, 以及它們的測試程式:
    Fred.h
    #ifndef Fred_H
    #define Fred_H
    
    class Fred
    {
    public:
        Fred();
        ~Fred();
        void service();
    private:
        static int m_serialID;
        const int m_objectID;
    };
    
    #endif
    

Fred.cpp #include "Fred.h" #include <iostream> using namespace std; int Fred::m_serialID = 0; Fred::Fred():m_objectID(m_serialID++) { cout << "Fred::ctor ID=" << m_objectID << endl; } Fred::~Fred() { cout << "Fred::dtor ID=" << m_objectID << endl; } void Fred::service() { cout << "Fred::service() ID=" << m_objectID << endl; }

Fred 類別是一個自定的類別, 這個類別本身沒有太多的功能, 最主要是為了配合 FredPtr 類別來運作, 顯示 FredPtr 類別的功能的。

下面的 FredPtr 類別是一個 Managed pointer, 基本上把一個 Fred 物件的指標包裝起來, 基本的使用方法和 Fred 的指標是一樣的, 但是增加了資源管理的功能, 在解構 FredPtr 物件時, 自動幫程式設計者刪除指標所指到的 Fred 物件。 有的時候寫程式的人動態地配置了一塊記憶體, 但是很容易忘記釋放它; 或是在運用 C++ Exception Handling 時, 由於 Exception 的發生, 控制權跳過了正常釋放的程式碼, 此時若是運用下面這個 FredPtr 類別就能夠自動地釋放所配置的記憶體:

    FredPtr.h
    #ifndef FredPtr_h
    #define Fredptr_h
    
    #include "Fred.h"
    
    class FredPtr
    {
    public:
        FredPtr(Fred* ptr=0);
        ~FredPtr();
        void deallocate();
        FredPtr& operator=(Fred* ptr);
        Fred* operator->();
        Fred& operator*();
        Fred* relinguishOwnership();
    private:
        Fred* m_ptr;
        FredPtr& operator=(const FredPtr &); // unimplemented
        FredPtr(const FredPtr &);            // unimplemented
    };
    
    #endif

FredPtr.cpp #include "FredPtr.h" #include <assert.h> FredPtr::FredPtr(Fred* ptr) : m_ptr(ptr) { } FredPtr::~FredPtr() { deallocate(); } void FredPtr::deallocate() { delete m_ptr; m_ptr = 0; } FredPtr& FredPtr::operator=(Fred* ptr) { deallocate(); m_ptr = ptr; return *this; } Fred* FredPtr::operator->() { assert(m_ptr != 0); return m_ptr; } Fred& FredPtr::operator*() { assert(m_ptr != 0); return *m_ptr; } // 使得 FredPtr 物件不再擁有一 Fred 物件 Fred* FredPtr::relinguishOwnership() { Fred* old = m_ptr; m_ptr = 0; return old; }

下面是 FredPtr 類別的測試程式: 請注意: 一個 FredPtr 物件可以擁有一個 Fred 物件, 同時在清除這個指標的內容時也必須負責這個物件的刪除, 由這個物件中拷貝指標內容時也必須透過 relinguishOwnership() 這個成員函式順道取得對 Fred 物件的擁有權:
    
    TestFredPtr01.cpp
    #include "Fred.h"
    #include "FredPtr.h"
    #include <iostream>
    using namespace std;
    
    void main()
    {
        FredPtr ptrFred1, ptrFred2(new Fred);
        // ptrFred2 擁有一 Fred 物件 (ID=0)
    
        ptrFred1 = new Fred; // ptrFred1 擁有該 Fred 物件 (ID=1)
    
        ptrFred1 = 0; // 刪除該 Fred 物件 (ID=1)
    
        ptrFred2->service();
        (*ptrFred2).service();
    
        Fred *ptrFred;
        ptrFred = ptrFred2.relinguishOwnership();
        delete ptrFred;     // without this line, memory is
                            //  leaking 刪除 Fred 物件 (ID=0)
    }
範例執行程式

執行結果範例:

    Fred::ctor ID=0
    Fred::ctor ID=1
    Fred::dtor ID=1
    Fred::service() ID=0
    Fred::service() ID=0
    Fred::dtor ID=0
步驟二 注意在步驟一中 FredPtr 類別的 Assignment operator 和 copy ctor 都沒有實作, 同時也把它們設為 private 成員函式, 以避免不小心誤用

我們先來嘗試製作這兩個成員函式, 首先是 assignment operator: FredPtr& operator=(const FredPtr&); 實作這個函式最主要的目的是達成物件的設定, 例如:

    FredPtr ptr1(new Fred), ptr2(new Fred);
    ...
    ptr1 = ptr2;
在這個動作中, ptr1 中原來所掌管的 Fred 物件的指標會被覆蓋掉, 所以同時就應該把這個 Fred 物件刪除掉, 而做完這個動作以後 ptr1 和 ptr2 如果是指向相同的 Fred 物件的話, 究竟該由 ptr1 還是 ptr2 來管理 Fred 物件呢?

所以在這裡我們實作的時候乾脆拷貝一份, 各自管理自己的 Fred 物件 (請自己先實作看看):


    FredPtr& FredPtr::operator=(const FredPtr &rhs)
    {
        if (&rhs == this) return *this;
        deallocate();
        if (rhs.m_ptr)
            m_ptr = new Fred(*rhs.m_ptr);
        else
            m_ptr = 0;
        return *this;
    }
請注意這個實作和一般的設定的運算有相當的差異性存在, 這也是為什麼原來的 FredPtr 並不想實作 assignment operator 的原因之一

請把 assignment operator 設為 public 成員函式

接下來我們來考量拷貝建構元 (copy ctor), 這個成員函式最主要由一個已經建構好的 FredPtr 物件建構出一個新的物件, 因為需要擁有所管理的 Fred 物件, 所以在這個成員函式中也需要以原來的 FredPtr 物件所管理的 Fred 物件為範本, 建立一個新的 Fred 物件, (請自己先實作看看):


    FredPtr::FredPtr(const FredPtr &src)
    {
        if (src.m_ptr)
            m_ptr = new Fred(*src.m_ptr);
        else
            m_ptr = 0;
    }

請把拷貝建構元設為 public 成員函式

步驟三 完成上一步驟後我們可以用下面的主程式來測試一下:
    #include "Fred.h"
    #include "FredPtr.h"
    #include <iostream>
    using namespace std;
    
    void main()
    {
        FredPtr ptrFred1, ptrFred2(new Fred);
        FredPtr ptrFred3(new Fred);
    
        ptrFred1 = new Fred;
        ptrFred1 = 0;
    
        ptrFred1 = new Fred;
        ptrFred1 = ptrFred3;
        
        FredPtr ptrFred4 = ptrFred1;
    
        ptrFred2->service();
        (*ptrFred2).service();
    
        Fred *ptrFred;
        ptrFred = ptrFred2.relinguishOwnership();
        delete ptrFred; // without this line, memory is leaking
    }
範例執行程式

測試結果如下:

    Fred::ctor ID=0
    Fred::ctor ID=1
    Fred::ctor ID=2
    Fred::dtor ID=2
    Fred::ctor ID=3
    Fred::dtor ID=3
    Fred::copy ctor ID=4
    Fred::copy ctor ID=5
    Fred::service() ID=0
    Fred::service() ID=0
    Fred::dtor ID=0
步驟四 接下來我們來看看對於這種 Managed Pointer 類別的新需求:
      假設我們的程式裡有一個 Wilson 的類別, 我們也希望
      能夠像 FredPtr 一樣有一個 Managed Pointer 類別能夠 
      包裝 Wilson 物件的指標
最簡單的方法當然就是拷貝步驟一、二中的 FredPtr 類別改成 WilsonPtr 類別, 不過如果我們的程式裡還有很多其它的類別也都需要 Managed Pointer 的話, 這個拷貝的動作就顯得很不聰明了, 萬一以後發現 Managed Pointer 的機制需要修改的話, 需要把每一個重複的地方都改好, 否則就會留下不幸的 bug 在程式裡了!!

所以我們要運用樣板類別 (template) 的語法, 修改步驟一、二中的 FredPtr 成為 HeapPtr 類別, 使得 compiler 可以根據 HeapPtr 樣板自動幫我們產生新的 Managed Pointer 類別, 就好像我們之前使用 vector<int>, vector<Fred>, vector<double *> 一樣, 我們可以用 HeapPtr<Fred>, HeapPtr<Wilson>, HeapPtr<int>, 每次你做一個新的組合 HeapPtr<NewClass>, compiler 就幫你合成一個新的 Managed Pointer 類別, 最主要的修改方法如下:

  1. 將類別名稱 FredPtr 改為 HeapPtr
  2. 類別宣告前增加 template<class T>
  3. 類別中使用到 Fred 類別的地方改為參數 T (沿用 Fred 也行, 在上一步就用 template<class Fred>)
  4. 將所有在 FredPtr.cpp 中定義的 FredPtr 類別成員函式都移到 HeapPtr.h 中
  5. 類別成員函式定義前增加 template<class T>
  6. 將 scope operator FredPtr:: 改為 HeapPtr<T>::
  7. 將用到 FredPtr 型態的宣告改為 HeapPtr<T> (compiler 並沒有要求把每一個 FredPtr 型態都改成 HeapPtr<T>, 但是我不太確定規則!!! 你也可以在 compiler 發出錯誤訊息時再改)
步驟五 我們可以用下面的程式來測試一下上面的 template 程式
    #include "Fred.h"
    #include "Wilson.h"
    #include "HeapPtr.h"
    #include <iostream>
    using namespace std;

    void main()
    {
        HeapPtr<Fred> ptrFred1, ptrFred2(new Fred);
        HeapPtr<Fred> ptrFred3(new Fred);

        ptrFred1 = new Fred;
        ptrFred1 = 0;

        ptrFred1 = new Fred;
        ptrFred1 = ptrFred3;

        HeapPtr<Fred> ptrFred4 = ptrFred1;

        ptrFred2->service();
        (*ptrFred2).service();

        Fred *ptrFred;
        ptrFred = ptrFred2.relinguishOwnership();
        delete ptrFred; // without this line, memory is leaking

        HeapPtr<Wilson> ptrWilson(new Wilson);
        ptrWilson->service();

        HeapPtr<int> ptrIntAry(new int);
    }
範例執行程式

測試結果如下:

    Fred::ctor ID=0
    Fred::ctor ID=1
    Fred::ctor ID=2
    Fred::dtor ID=2
    Fred::ctor ID=3
    Fred::dtor ID=3
    Fred::copy ctor ID=4
    Fred::copy ctor ID=5
    Fred::service() ID=0
    Fred::service() ID=0
    Fred::dtor ID=0
    Wilson::ctor ID=0
    Wilson::service() ID=0
步驟六 你可以用 VC 中編譯的選項來找看看 Compiler 自動產生的類別的程式碼, VC6: Project / Setting / C/C++ / Category: Listing Files / Listing File Type: Assembly With Source Code 或是 VC2008: 專案 / xxx 屬性 / 組態屬性 / C/C++ / 輸出檔 / 組合語言輸出:有原始程式碼的組譯檔, 重新編譯, 檢視產生出來的組合語言檔案 *.asm, 你應該可以在 TestHeapPtr.asm 中找到 compiler 針對 HeapPtr<Fred>, HeapPtr<Wilson>, 和 HeapPtr<int> 所產生的程式碼, 例如下面的組合語言函式是 HeapPtr<Wilson> 類別的解構元函式 ~HeapPtr<Wilson>()

    ; HeapPtr<Wilson>::~HeapPtr<Wilson>, COMDAT
    ??1?$HeapPtr@VWilson@@@@QAE@XZ PROC NEAR 
    ; Line 31
    	push	ebp
    	mov	ebp, esp
    	push	ecx
    	mov	DWORD PTR _this$[ebp], ecx
    ; Line 32
    	mov	ecx, DWORD PTR _this$[ebp]
        ; HeapPtr<Wilson>::deallocate
    	call	?deallocate@?$HeapPtr@VWilson@@@@QAEXXZ
    ; Line 33
    	mov	esp, ebp
    	pop	ebp
    	ret	0
    ??1?$HeapPtr@VWilson@@@@QAE@XZ ENDP
    ; HeapPtr<Wilson>::~HeapPtr<Wilson>
步驟七 請助教檢查後, 將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來, 選擇 Lab14-1 上傳, 後面的實習課程可能需要使用這裡所完成的程式

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

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