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

 
實習目標 練習撰寫 樣板類別 (template class)
特別注意 樣板類別 的程式碼應該放在 *.h 檔案中
請觀察撰寫 樣板類別 時所出現的語法錯誤
請觀察有 樣板類別 時的執行檔大小
 
步驟一 在這個練習中我們逐步地觀察為什麼需要撰寫 樣板類別, 並且將一個現成的類別改寫為 樣板類別。 首先, 我們先瞭解下面 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 物件的指標包裝起來, 希望對於寫程式的人來說這個類別的物件的使用方法和 C/C++ 內建的 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 物件呢? (因為解構 FredPtr 物件時釋放所管理的 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 的話, 這個拷貝的動作就造成程式碼的重複 (redundancy), 而顯得很不聰明了, 萬一以後發現 Managed Pointer 的機制需要修改的話, 需要把每一個重複的地方都改好, 否則就會留下不幸的 bug 在程式裡了!!

這裡我們要運用樣板類別 (template class) 的語法, 修改步驟一、二中的 FredPtr 成為 HeapPtr 類別, 使得編譯器可以根據 HeapPtr 樣板自動幫我們產生新的 Managed Pointer 類別, 就好像我們之前使用 vector<int>, vector<Fred>, vector<double *> 一樣, 我們可以用 HeapPtr<Fred>, HeapPtr<Wilson>, HeapPtr<int>, 每次你做一個新的組合 HeapPtr<NewClass>, 編譯器就幫你合成一個新的 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> , 函式裡使用到 Fred 的地方改成參數 T
  6. 將 scope operator FredPtr:: 改為 HeapPtr<T>::
  7. 將用到 FredPtr 型態的宣告改為 HeapPtr<T> (VC2010 並沒有要求把每一個 FredPtr 型態都改成 HeapPtr<T>, 也可以是 HeapPtr, 但是我不太確定規則!!! 你也可以在編譯器產生錯誤訊息時再改)
步驟五

Wilson.h

#ifndef Wilson_H
#define Wilson_H

class Wilson
{
public:
    Wilson();
    Wilson(Wilson &);
    ~Wilson();
    void service();
private:
    static int m_serialID;
    const int m_objectID;
};

#endif

Wilson.cpp

#include "Wilson.h"

#include <iostream>
using namespace std;

int Wilson::m_serialID = 0;

Wilson::Wilson():m_objectID(m_serialID++)
{
    cout << "Wilson::ctor ID=" << m_objectID << endl; 
}

Wilson::~Wilson()
{
    cout << "Wilson::dtor ID=" << m_objectID << endl;
}

Wilson::Wilson(Wilson&):m_objectID(m_serialID++)
{
    cout << "Wilson::copy ctor ID=" << m_objectID << endl; 
}

void Wilson::service()
{
    cout << "Wilson::service() ID=" << m_objectID << endl; 
}

我們可以用下面的程式來測試一下上面的樣板類別

    #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 中編譯的選項來看看編譯器自動產生的類別的程式碼,

  • VC6: Project / Setting / C/C++ / Category: Listing Files / Listing File Type: Assembly With Source Code 或是
  • VC2010: 專案 / xxx 屬性 / 組態屬性 / C/C++ / 輸出檔 / 組合語言輸出:有原始程式碼的組譯檔,

重新編譯, 檢視產生出來的組合語言檔案 *.asm, 你應該可以在 TestHeapPtr.asm 中找到編譯器針對 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>

請注意: 對於 C++ 來說 template 是一個比較晚才加入的功能, 如果你在兩個模組 (.cpp) 中都使用到 vector<int>, 編譯器其實幫你產生了兩份 vector<int> 的類別, 低階的類別名稱一模一樣, 所以對於這兩個模組來說 vector<int> 是相同的型態, 比較特別的是這兩份的程式碼都給 linker 以後, linker 並不會說它們重複定義, 這兩個 vector<int> 會標示為 Weak symbols (local), 就好像連結 library 時, 如果有重複的函式 linker 也不會說重複定義, 以先找到的為準

步驟七 請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 壓縮起來, 選擇 Lab14-1 上傳, 後面的實習課程可能需要使用這裡所完成的程式
步驟八 有了樣板 (template) 的語法以後, 你除了可以製作樣板全域函式 (templated toplevel function) 以及樣板類別 (templated class)之外, 也可以製作樣板成員函式 (templated member function), 例如:
//------ MyClass.h -----
class MyClass
{
public:
    MyClass(void);
    template <class T> void func(T x);
};

#include <iostream>
#include <iomanip>
template <class T> void MyClass::func(T x)
{
    std::cout << "in func(" << typeid(x).name() << "): "
<< x << std::endl; } //------ end of MyClass.h -----

這樣子的設計比較奇怪的地方是一個物件的介面是可以沒有限定個數的, 如果有兩個客戶都使用相同的 MyClass 類別的物件, 但是使用 x.func(r) 的時候, r 的型態不同, 此時兩個 MyClass 是相同的嗎? 請想辦法測試一下 (testTemplate1.rar), 詳細測試以後會發現 linker 能夠組合各個模組裡面重複的 MyClass::func(T), 如果 T 是相同的, 因為是 Weak symbol, 所以 linker 隨便挑選一份, 如果 T 不同, 則 linker 會統一組合成 MyClass

請參考 testTemplate.rar

請注意這個 testTemplate 專案你下載以後是沒有辦法直接建置成功的, 因為我故意把上面 void MyClass::func(T x){ ... }的定義放在 MyClass.cpp 裡, 請特別注意連結器的錯誤訊息, 請適當修改以後再測試

當 template <class T> void MyClass::func(T x) { ... }
沒有定義在 MyClass.h 檔案中時, 因為 main() 中使用 x.func(r) 和 x.func(s),
編譯器在編譯 main.cpp 時, 只看到 MyClass.h 裡面類別的宣告,
沒有看到你寫在 MyClass.cpp 裡的 template <class T> void MyClass::func(T x) { ... }
所以不會幫你產生 func<double>() 和 func<int>() 兩個成員函式, 因此
會出現下面兩個連結錯誤:

1> LINK : 最後的累加連結找不到或未建置 C:\user\pyting\testTemplate\Debug\testTemplate.exe,正在執行完整連結
1>main.obj : error LNK2019: 無法解析的外部符號 "public: void __thiscall MyClass::func<double>(double)" (??$func@N@MyClass@@QAEXN@Z) 在函式 _main 中被參考
1>main.obj : error LNK2019: 無法解析的外部符號 "public: void __thiscall MyClass::func<int>(int)" (??$func@H@MyClass@@QAEXH@Z) 在函式 _main 中被參考
1>C:\user\pyting\testTemplate\Debug\testTemplate.exe : fatal error LNK1120: 2 個無法解析的外部符號

不過 func<char *>(char *) 因為是在 MyClass() 函式中使用到的,
不管你 template <class T> void MyClass::func(T x)
是定義在 MyClass.h 或是 MyClass.cpp 中,
compiler 在編譯 MyClass.cpp 時是一定會產生的


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

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