程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> ATL布幔之下的秘密(3)

ATL布幔之下的秘密(3)

編輯:關於VC++

介紹

如果你是個模板的高手,你就可以將ATL的學習作為一種享受。 在這一節中,我將要嘗試解釋一些ATL使用的模板技術。我不能保證你讀完本節 後能成為一個模板高手,只能是盡我所能讓你在讀完本文後能夠更輕松地理解 ATL的源碼。

程序35.

#include <iostream>
using namespace std;
template <typename T>
T Maximum(const T& a, const T& b) {
 return a > b ? a : b;
}
int main() {
 cout << Maximum(5, 10) << endl;
 cout << Maximum(''A'', ''B'') << endl;
 return 0;
}

程序的輸出為:

10
B

在這裡,由於模板 函數的關系,我們就沒有必要分別重載int和char數據類型的函數版本了。其中 很重要的一點是,函數的兩個參數類型必須一致。但是如果我們傳入了不同的數 據類型,我們就需要告知編譯器應該把這個參數考慮為哪種數據類型。

程序36.

#include <iostream>
using namespace std;
template <typename T>
T Maximum(const T& a, const T& b) {
 return a > b ? a : b;
}
int main() {
 cout << Maximum<int>(5, ''B'') << endl;
 cout << Maximum<char>(5, ''B'') << endl;
  return 0;
}

程序的輸出為:

66
B

我們也可以編寫類模板,下面就是一個簡單版本的堆棧類模板。

程序 37.

#include <iostream>
using namespace std;
template <typename T>
class Stack {
private:
  T* m_pData;
 int m_iTop;
public:
 Stack(int p_iSize = 0) : m_iTop(0) {
  m_pData = new T[p_iSize];
 }
  void Push(T p_iData) {
  m_pData[m_iTop++] = p_iData;
 }
 T Pop() {
  return m_pData[--m_iTop];
 }
 T Top() {
  return m_pData[m_iTop];
 }
 ~Stack() {
  if (m_pData) {
   delete [] m_pData;
  }
 }
private:
 Stack(const Stack<T>&);
  Stack<T>& operator = (const Stack<T>&);
};
int main() {
 Stack<int> a(10);
 a.Push(10);
 a.Push(20);
 a.Push(30);
 cout << a.Pop() << endl;
 cout << a.Pop() << endl;
 cout << a.Pop() << endl;

 return 0;
}

這個程序 中沒有任何錯誤檢驗,不過這個程序的目的只是示范模板的用法,而不是真的要 寫一個專業的堆棧類。

程序的輸出為:

30
20
10

我們也可以將數據類型作為一個模板參數來傳遞,並且為它設置 一個默認值。讓我們來稍微修改一下程序37(譯注:原文為“程序 36”,應為37),並將堆棧的尺寸作為一個模板參數來傳遞,而不是作為 構造函數的參數。

程序38.

#include <iostream>
using namespace std;
template <typename T, int iSize = 10>
class Stack {
private:
 T m_pData[iSize];
 int m_iTop;
public:
 Stack() : m_iTop(0) {
 }
 void Push(T p_iData) {
  m_pData[m_iTop++] = p_iData;
  }
 T Pop() {
  return m_pData[--m_iTop];
 }
  T Top() {
  return m_pData[m_iTop];
 }
private:
 Stack(const Stack<T>&);
 Stack<T>& operator = (const Stack<T>&);
};
int main() {
 Stack<int, 10> a;
 a.Push(10);
 a.Push(20);
 a.Push(30);
 cout << a.Pop() << endl;
  cout << a.Pop() << endl;
 cout << a.Pop() << endl;

 return 0;
}

程序的輸出和前一 個相同。這個程序最重要的一點為:

template <typename T, int iSize = 10>

現在就有一個問題:哪一個更好呢?通常,傳遞模板 參數的辦法是優於給構造函數傳遞參數的。為什麼呢?因為在你將堆棧尺寸作為 模板參數傳遞的時候,這個給定數據類型的數組就會被自動創建;而給構造函數 傳遞參數則意味著構造函數會在運行時使用new或malloc一系列功能來分配內存 。如果我們已經確定在創建好堆棧之後就不再更改它的尺寸(就像上面程序中 private段中拷貝構造函數和賦值運算符中的那樣)了,那麼無疑使用模板參數 是更加適合的。

(譯注:作者Amjad在上面兩個程序中並未實現拷貝構造 函數和賦值運算符,這大概是由於這兩者對於本文的內容無關緊要之故吧。在此 我要指出的是正如作者所說,“不是真的要寫一個專業的堆棧類”、 “沒有任何錯誤檢驗”,並且這其中類的組織結構使得精確實現拷貝 構造函數和賦值運算符有一定的難度,尤其是程序37——我們無法從 一個已經定義好的堆棧獲得它的最大容量。)

你也可以將用戶定義的類 作為一個類型參數來傳遞,但是請確認這個類擁有在那個模板函數或類模板中重 載的所有運算符。

例如,請看程序35那個求最大值的函數。這個程序使 用了一個operator >,所以如果我們傳遞自己的類的話,那麼這個類必須重 載了>運算符。下面這個例子示范了這一點。

程序 39.

#include <iostream>
using namespace std;
template <typename T>
T Maximum(const T& a, const T& b) {
 return a > b ? a : b;
}
class Point {
private:
 int m_x, m_y;
public:
 Point(int p_x = 0, int p_y = 0) : m_x(p_x), m_y(p_y) {
 }
 bool friend operator > (const Point& lhs, const Point& rhs) {
   return lhs.m_x > rhs.m_x && lhs.m_y > rhs.m_y;
 }
 friend ostream& operator << (ostream& os, const Point& p) {
  return os << "(" << p.m_x << ", " << p.m_y << ") ";
 }
};
int main() {
 Point a(5, 10), b (15, 20);
 cout << Maximum(a, b) << endl;
  return 0;
}

程序的輸出為:

(15, 20)

同 樣,我們也能夠將一個類模板作為一個模板參數傳遞。現在讓我們來編寫這樣一 個Point類,並將其作為一個模板參數傳遞給Stack類模板。

程序 40.

#include <iostream>
using namespace std;
template <typename T>
class Point {
private:
  T m_x, m_y;
public:
 Point(T p_x = 0, T p_y = 0) : m_x (p_x), m_y(p_y) {
 }
 bool friend operator > (const Point<T>& lhs, const Point<T>& rhs) {
   return lhs.m_x > rhs.m_x && lhs.m_y > rhs.m_y;
 }
 friend ostream& operator << (ostream& os, const Point<T>& p) {
  return os << "(" << p.m_x << ", " << p.m_y << ")";
 }
};
template <typename T, int iSize = 10>
class Stack {
private:
 T m_pData [iSize];
 int m_iTop;
public:
 Stack() : m_iTop(0) {
 }
 void Push(T p_iData) {
  m_pData[m_iTop++] = p_iData;
 }
 T Pop() {
  return m_pData[-- m_iTop];
 }
 T Top() {
  return m_pData[m_iTop];
 }
private:
 Stack(const Stack<T>&);
  Stack<T>& operator = (const Stack<T>&);
};
int main() {
 Stack<Point<int> > st;
  st.Push(Point<int>(5, 10));
 st.Push(Point<int>(15, 20));
 cout << st.Pop() << endl;
 cout << st.Pop() << endl;
 return 0;
}

程序 的輸出為:

(15, 20)
(5, 10)

這個程序中最重要的 部分為:

Stack<Point<int> > st;

在這裡, 你必須在兩個大於號之間放置一個空格,否則編譯器就會將它看作>>(右 移運算符)並產生錯誤。

對於這個程序我們還可以這麼做,就是為模板 參數傳遞默認的類型值,也就是將

template <typename T, int iSize = 10>

換為

template <typename T = int, int iSize = 10>

現在我們就沒有必要一定在創建Stack類對 象的時候傳遞數據類型了,但是你仍然需要書寫這一對尖括弧以告知編譯器使用 默認的數據類型。你可以這麼創建對象:

Stack<> st;

當你在類的外部定義(譯注:原文此處是“declare” ,我以為應該是“define”更准確一些。)類模板的成員函數的時候 ,你仍然需要寫出帶有模板參數的類模板全稱。

程序 41.

#include <iostream>
using namespace std;
template <typename T>
class Point {
private:
  T m_x, m_y;
public:
 Point(T p_x = 0, T p_y = 0);
  void Setxy(T p_x, T p_y);
 T getX() const;
 T getY() const;
 friend ostream& operator << (ostream& os, const Point<T>& p) {
  return os << " (" << p.m_x << ", " << p.m_y  << ")";
 }
};
template <typename T>
Point<T>::Point(T p_x, T p_y) : m_x(p_x), m_y(p_y) {
}
template <typename T>
void Point<T>::Setxy(T p_x, T p_y) {
 m_x = p_x;
 m_y = p_y;
}
template <typename T>
T Point<T>::getX() const {
 return m_x;
}
template <typename T>
T Point<T>::getY() const {
 return m_y;
}
int main() {
 Point<int> p;
  p.Setxy(20, 30);
 cout << p << endl;
 return 0;
}

程序的輸出為:

(20, 30)

讓我們來 稍微修改一下程序35,傳遞字符串值(而不是int或float)作為參數,並看看結 果吧。

程序42.

#include <iostream>
using namespace std;
template <typename T>
T Maximum(T a, T b) {
 return a > b ? a : b;
}
int main() {
  cout << Maximum("Pakistan", "Karachi") << endl;

 return 0;
}

程序的輸出為 Karachi。(譯注:在我的Visual Studio.net 2003下的輸出卻為Pakistan,這 不同的原因是編譯器組織字符串地址的方式不同決定的,但是Maximum函數的結 果是應該返回內存高位的那個地址的,這和作者說的道理是一致的。)為什麼呢 ?因為這裡char*作為模板參數傳遞, Karachi在內存中存儲的位置更高,而 >運算符僅僅比較這兩個地址值而不是字符串本身。

那麼,如果我們 希望基於字符串的長度來比較而不是地址的話,應該怎麼做呢?

解決的 辦法是對char*數據類型進行模板的特化。下面是一個模板特化的例子。

程序43.

#include <iostream>
using namespace std;
template <typename T>
T Maximum(T a, T b) {
 return a > b ? a : b;
}
template <>
char* Maximum(char* a, char* b) {
 return strlen(a) > strlen(b) ? a : b;
}
int main() {
 cout << Maximum ("Pakistan", "Karachi") << endl;

 return 0;
}

至於類模板,也可以用相同的辦法進行特化。

程序44.

#include <iostream>
using namespace std;
template <typename T>
class TestClass {
public:
 void F(T pT) {
  cout << "T version" << ''\t'';
  cout << pT << endl;
 }
};
template <>
class TestClass<int> {
public:
 void F(int pT) {
   cout << "int version" << ''\t'';
  cout << pT << endl;
 }
};
int main() {
 TestClass<char> obj1;
 TestClass<int> obj2;
 obj1.F(''A'');
 obj2.F(10);
 return 0;
}

程序的輸出為:

T version A
int version 10

ATL中就有若干類是 類似這樣的特化版本,例如在ATLBASE.H中定義的CComQIPtr。

模板也可 以在不同的設計模式中使用,例如策略設計模式可以使用模板實現。

程 序45.

#include <iostream>
using namespace std;
class Round1 {
public:
 void Play() {
  cout << "Round1::Play" << endl;
 }
};
class Round2 {
public:
 void Play() {
  cout << "Round2::Play" << endl;
 }
};
template <typename T>
class Strategy {
private:
 T objT;
public:
 void Play() {
  objT.Play();
 }
};
int main() {
 Strategy<Round1> obj1;
 Strategy<Round2> obj2;
 obj1.Play();
 obj2.Play ();
 return 0;
}

在這裡,Round1和Round2為一個游戲 中不同的關卡類,並且Strategy類依靠傳遞的模板參數來決定該做些什麼。

程序的輸出為:

Round1::Play
Round2::Play

ATL就是使用Strategy設計模式來實現線程的。

代理設計模式也可以使用模板實現,智能指針就是一個例子。下面就是 一個沒有使用模板的簡單版本智能指針。

程序46.

#include <iostream>
using namespace std;
class Inner {
public:
 void Fun() {
  cout << "Inner::Fun" << endl;
 }
};
class Outer {
private:
 Inner* m_pInner;
public:
  Outer(Inner* p_pInner) : m_pInner(p_pInner) {
 }
 Inner* operator -> () {
  return m_pInner;
 }
};
int main() {
 Inner objInner;
 Outer objOuter (&objInner);
 objOuter->Fun();
 return 0;
}

程序的輸出為:

Inner::Fun()

簡單地說來,我 們僅僅重載了->運算符,但是在實際的智能指針中,所有必須的運算符(例 如=、==、!、&、*)都需要被重載。以上的智能指針有一個大問題:它只能 包含指向Inner對象的指針。我們可以編寫Outer類模板來取消這一限制,現在讓 我們來略微修改一下程序。

程序47.

#include <iostream>
using namespace std;
class Inner {
public:
 void Fun() {
  cout << "Inner::Fun" << endl;
 }
};
template <typename T>
class Outer {
private:
 T* m_pInner;
public:
 Outer(T* p_pInner) : m_pInner(p_pInner) {
 }
 T* operator -> () {
  return m_pInner;
 }
};
int main() {
 Inner objInner;
  Outer<Inner> objOuter(&objInner);
 objOuter->Fun ();
 return 0;
}

程序的輸出和前一個一樣,但是現在 Outer類就可以包含任何類型了,只需要把類型作為模板參數傳遞進來即可。

ATL中有兩個智能指針,CComPtr和CComQIPtr。

你可以用模板做 一些有趣的事情,例如你的類可以在不同的情況下成為不同基類的子類。

程序48.

#include <iostream>
using namespace std;
class Base1 {
public:
 Base1() {
  cout << "Base1::Base1" << endl;
 }
};
class Base2 {
public:
 Base2() {
  cout << "Base2::Base2" << endl;
 }
};
template <typename T>
class Drive : public T {
public:
  Drive() {
  cout << "Drive::Drive" << endl;
 }
};
int main() {
 Drive<Base1> obj1;
 Drive<Base2> obj2;
 return 0;
}

程序的輸出為:

Base1::Base1
Drive::Drive
Base2::Base2
Drive::Drive

在這裡,Drive類是繼承自Base1 還是Base2是由在對象創建的時候傳遞給模板的參數決定的。

ATL也使用 了這一技術。當你使用ATL創建COM組件的時候,CComObject就會繼承自你的類。 在這裡ATL利用了模板,因為它不會預先知道你用來作COM組件而創建的類的名稱 。CComObject類定義於ATLCOM.H文件之中。

在模板的幫助下,我們也可 以模擬虛函數。現在讓我們重新回憶一下虛函數,下面是一個簡單的例子。

程序49.

#include <iostream>
using namespace std;
class Base {
public:
 virtual void fun() {
  cout << "Base::fun" << endl;
 }
 void doSomething() {
  fun();
 }
};
class Drive : public Base {
public:
 void fun() {
  cout << "Drive::fun" << endl;
 }
};
int main() {
 Drive obj;
 obj.doSomething();
  return 0;
}

程序的輸出為:

Drive::fun

在模板的幫助下,我們可以實現與之相同的行為。

程序 50.

#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
  void fun() {
  cout << "Base::fun" << endl;
 }
 void doSomething() {
  T* pT = static_cast<T*>(this);
  pT->fun();
 }
};
class Drive : public Base<Drive> {
public:
 void fun() {
  cout << "Drive::fun" << endl;
 }
};
int main() {
 Drive obj;
  obj.doSomething();
 return 0;
}

程序的輸出和前一個 是一樣的,所以我們可以用模板來模擬虛函數的行為。

程序中一個有趣 的地方為

class Drive : public Base<Drive> {

這 表明我們可以將Drive類作為一個模板參數來傳遞。程序中另外一個有趣的地方 是基類中的doSomething函數。

T* pT = static_cast<T*> (this);
pT->fun();

在這裡基類的指針被轉換為派生類的指 針,因為派生類是作為Base類的模板參數傳遞的。這個函數可以通過指針來執行 ,由於指針指向了派生類的對象,所以派生類的對象就被調用了。

但是 這就有一個問題了:我們為什麼要這樣做?答案是:這樣可以節省虛函數帶有的 額外開銷,也就是虛函數表指針、虛函數表以及節省了調用虛函數所花費的額外 時間。這就是ATL中使組件盡可能小、盡可能快的主要思想。

現在,你的 腦海中可能會浮現另外一個問題。如果依靠這一開銷更少的技術可以模擬虛函數 的話,那我們為什麼還要調用虛函數呢?我們不應該用這一技術替換所有的虛函 數嗎?對於這一問題,我可以簡短地回答你:不,我們不能用這一技術替換虛函 數。

其實這一技術還存在一些問題。第一,你不能從Drive類進行更深層 的繼承,如果你試著這麼做,那麼它將不再會是虛函數的行為了。而對於虛函數 來說,這一切就不會發生。一旦你將函數聲明為虛函數,那麼在派生類中的所有 函數都會成為虛函數,無論繼承鏈有多深。現在我們看看當從Drive中再繼承一 個類的時候會發生什麼。

程序51.

#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
 void fun() {
  cout << "Base::fun" << endl;
 }
 void doSomething() {
  T* pT = static_cast<T*>(this);
   pT->fun();
 }
};
class Drive : public Base<Drive> {
public:
 void fun() {
  cout << "Drive::fun" << endl;
 }
};
class MostDrive : public Drive {
public:
 void fun() {
  cout << "MostDrive::fun" << endl;
 }
};
int main() {
 MostDrive obj;
  obj.doSomething();
 return 0;
}

程序的輸出和前一個 一樣。但是對於虛函數的情況來說,輸出就應該是:

MostDrive::fun

這一技術還有另外一個問題,就是當我們 使用Base類的指針來存儲派生類的地址的時候。

程序 52.

#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
  void fun() {
  cout << "Base::fun" << endl;
 }
 void doSomething() {
  T* pT = static_cast<T*>(this);
  pT->fun();
 }
};
class Drive : public Base<Drive> {
public:
 void fun() {
  cout << "Drive::fun" << endl;
 }
};
int main() {
 Base* pBase = NULL;
 pBase = new Drive;
 return 0;
}

這個程序會給出 一個錯誤,因為我們沒有向基類傳遞模板參數。現在我們稍微修改一下,並傳遞 模板參數。

程序53.

#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
 void fun() {
  cout << "Base::fun" << endl;
 }
 void doSomething () {
  T* pT = static_cast<T*>(this);
  pT- >fun();
 }
};
class Drive : public Base<Drive> {
public:
 void fun() {
  cout << "Drive::fun" << endl;
 }
};
int main() {
 Base<Drive>* pBase = NULL;
 pBase = new Drive;
 pBase->doSomething();
 return 0;
}

現在程序正常工作,並給出了我們所期望的輸出,也就是:

Drive::fun

但是在Base類有多個繼承的時候,就會出現問 題。為了更好地弄懂這一點,請看下面的程序。

程序 54.

#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
  void fun() {
  cout << "Base::fun" << endl;
 }
 void doSomething() {
  T* pT = static_cast<T*>(this);
  pT->fun();
 }
};
class Drive1 : public Base<Drive1> {
public:
 void fun() {
  cout << "Drive1::fun" << endl;
 }
};
class Drive2 : public Base<Drive2> {
public:
 void fun() {
  cout << "Drive2::fun" << endl;
 }
};
int main() {
 Base<Drive1>* pBase = NULL;
 pBase = new Drive1;
 pBase->doSomething();
 delete pBase;
 pBase = new Drive2;
 pBase->doSomething();
 return 0;
}

程序會在下面的代碼處給出錯誤:

pBase = new Drive2;

因為pBase是一個指向Base<Drive1>的指針,而不是 Base<Drive2>。簡單地說來,就是你不能使Base類的指針指向不同的 Drive類。換句話說,你不能使用Base類指針的數組存儲不同的派生類,而在虛 函數之中則是可行的。

希望在下一篇文章中能夠探究一些ATL的其它秘密 。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved