| <C++實(shí)踐系列>C++中的模板(template) 作者:張笑猛 提交者:eastvc 發(fā)布日期:2003-11-22 14:36:25 原文出處:http://objects.nease.net/ 網(wǎng)上我最喜歡的技術(shù)文章是類(lèi)似某何君所著(zhù)“CVS快速入門(mén)”或者“UML reference card”之類(lèi),簡(jiǎn)短扼要,可以非??斓念I(lǐng)著(zhù)你進(jìn)入一個(gè)新天地。而對于比較長(cháng)的文章我通常是將其保存到硬盤(pán)上,然后準備著(zhù)“以后有時(shí)間”的時(shí)候再看,但它們通常的命運都是“閑坐說(shuō)玄宗”,直到某一天在整理硬盤(pán)時(shí)將它們以“不知所云”入罪,一并刪除。 這篇小文主要是針對剛剛接觸模板概念的讀者,希望能幫助讀者學(xué)習模板的使用。為了避免本文也在諸公的硬盤(pán)上遭逢厄運,我決定寫(xiě)的短些。“以后有時(shí)間”的時(shí)候再補充些內容。 TOC 3. 使用技巧 1. 簡(jiǎn)介 模板是C++在90年代引進(jìn)的一個(gè)新概念,原本是為了對容器類(lèi)(container classes)的支持[1],但是現在模板產(chǎn)生的效果已經(jīng)遠非當初所能想象。 簡(jiǎn)單的講,模板就是一種參數化(parameterized)的類(lèi)或函數,也就是類(lèi)的形態(tài)(成員、方法、布局等)或者函數的形態(tài)(參數、返回值等)可以被參數改變。更加神奇的是這里所說(shuō)的參數,不光是我們傳統函數中所說(shuō)的數值形式的參數,還可以是一種類(lèi)型(實(shí)際上稍微有一些了解的人,更多的會(huì )注意到使用類(lèi)型作為參數,而往往忽略使用數值作為參數的情況)。 舉個(gè)常用的例子來(lái)解釋也許模板就從你腦袋里的一個(gè)模糊的概念變成活生生的代碼了: 在C語(yǔ)言中,如果我們要比較兩個(gè)數的大小,常常會(huì )定義兩個(gè)宏: #define min(a,b) ((a)>(b)?(b):(a)) 這樣你就可以在代碼中: return min(10, 4); 或者: return min(5.3, 18.6); 這兩個(gè)宏非常好用,但是在C++中,它們并不像在C中那樣受歡迎。宏因為沒(méi)有類(lèi)型檢查以及天生的不安全(例如如果代碼寫(xiě)為min(a++, b--);則顯然結果非你所愿),在C++中被inline函數替代。但是隨著(zhù)你將min/max改為函數,你立刻就會(huì )發(fā)現這個(gè)函數的局限性 —— 它不能處理你指定的類(lèi)型以外的其它類(lèi)型。例如你的min()聲明為: int min(int a, int b); 則它顯然不能處理float類(lèi)型的參數,但是原來(lái)的宏卻可以很好的工作!你隨后大概會(huì )想到函數重載,通過(guò)重載不同類(lèi)型的min()函數,你仍然可以使大部分代碼正常工作。實(shí)際上,C++對于這類(lèi)可以抽象的算法,提供了更好的辦法,就是模板: template <class T> const T & min(const T & t1, const T & t2) { 這是一個(gè)模板函數的例子。在有了模板之后,你就又自由了,可以像原來(lái)在C語(yǔ)言中使用你的min宏一樣來(lái)使用這個(gè)模板,例如: return min(10,4); 也可以: return min(5.3, 18.6) 你發(fā)現了么?你獲得了一個(gè)類(lèi)型安全的、而又可以支持任意類(lèi)型的min函數,它是否比min宏好呢? 當然上面這個(gè)例子只涉及了模板的一個(gè)方面,模板的作用遠不只是用來(lái)替代宏。實(shí)際上,模板是泛化編程(Generic Programming)的基礎。所謂的泛化編程,就是對抽象的算法的編程,泛化是指可以廣泛的適用于不同的數據類(lèi)型。例如我們上面提到的min算法。 2. 語(yǔ)法你千萬(wàn)不要以為我真的要講模板的語(yǔ)法,那太難為我了,我只是要說(shuō)一下如何聲明一個(gè)模板,如何定義一個(gè)模板以及常見(jiàn)的語(yǔ)法方面的問(wèn)題。 template<> 是模板的標志,在<>中,是模板的參數部分。參數可以是類(lèi)型,也可以是數值。例如: template<class T, T t> 在這個(gè)聲明中,第一個(gè)參數是一個(gè)類(lèi)型,第二個(gè)參數是一個(gè)數值。這里的數值,必須是一個(gè)常量。例如針對上面的聲明: Temp<int, 10> temp; // 合法 int i = 10; const int j = 10; 參數也可以有默認值: template<class T, class C=char> ... 默認值的規則與函數的默認值一樣,如果一個(gè)參數有默認值,則其后的每個(gè)參數都必須有默認值。 參數的名字在整個(gè)模板的作用域內有效,類(lèi)型參數可以作為作用域內變量的類(lèi)型(例如上例中的T t_),數值型參數可以參與計算,就象使用一個(gè)普通常數一樣(例如上例中的cout << t << endl)。 模板有個(gè)值得注意的地方,就是它的聲明方式。以前我一直認為模板的方法全部都是隱含為inline的,即使你沒(méi)有將其聲明為inline并將函數體放到了類(lèi)聲明以外。這是模板的聲明方式給我的錯覺(jué),實(shí)際上并非如此。我們先來(lái)看看它的聲明,一個(gè)作為接口出現在頭文件中的模板類(lèi),其所有方法也都必須與類(lèi)聲明出現在一起。用通俗的話(huà)來(lái)說(shuō),就是模板類(lèi)的函數體也必須出現在頭文件中(當然如果這個(gè)模板只被一個(gè)C++程序文件使用,它當然也可以放在.cc中,但同樣要求類(lèi)聲明與函數體必須出現在一起)。這種要求與inline的要求一樣,因此我一度認為它們隱含都是inline的。但是在Thinking In C++[2]中,明確的提到了模板的non-inline function,就讓我不得不改變自己的想法了??磥?lái)正確的理解應該是:與普通類(lèi)一樣,聲明為inline的,或者雖然沒(méi)有聲明為inline但是函數體在類(lèi)聲明中的才是inline函數。 澄清了inline的問(wèn)題候,我們再回頭來(lái)看那些我們寫(xiě)的包含了模板類(lèi)的丑陋的頭文件,由于上面提到的語(yǔ)法要求,頭文件中除了類(lèi)接口之外,到處充斥著(zhù)實(shí)現代碼,對用戶(hù)來(lái)說(shuō),十分的不可讀。為了能像傳統頭文件一樣,讓用戶(hù)盡量只看到接口,而不用看到實(shí)現方法,一般會(huì )將所有的方法實(shí)現部分,放在一個(gè)后綴為.i或者.inl的文件中,然后在模板類(lèi)的頭文件中包含這個(gè).i或者.inl文件。例如: // start of temp.h // start of temp.inl 通過(guò)這樣的變通,即滿(mǎn)足了語(yǔ)法的要求,也讓頭文件更加易讀。模板函數也是一樣。 普通的類(lèi)中,也可以有模板方法,例如: class A{ 對于模板方法的要求與模板類(lèi)的方法一樣,也需要與類(lèi)聲明出現在一起。而這個(gè)類(lèi)的其它方法,例如dummy(),則沒(méi)有這樣的要求。 3. 使用技巧知道了上面所說(shuō)的簡(jiǎn)單語(yǔ)法后,基本上就可以寫(xiě)出自己的模板了。但是在使用的時(shí)候還是有些技巧。 3.1 語(yǔ)法檢查對模板的語(yǔ)法檢查有一部分被延遲到使用時(shí)刻(類(lèi)被定義[3],或者函數被調用),而不是像普通的類(lèi)或者函數在被編譯器讀到的時(shí)候就會(huì )進(jìn)行語(yǔ)法檢查。因此,如果一個(gè)模板沒(méi)有被使用,則即使它包含了語(yǔ)法的錯誤,也會(huì )被編譯器忽略,這是語(yǔ)法檢查問(wèn)題的第一個(gè)方面,這不常遇到,因為你寫(xiě)了一個(gè)模板就是為了使用它的,一般不會(huì )放在那里不用。與語(yǔ)法檢查相關(guān)的另一個(gè)問(wèn)題是你可以在模板中做一些假設。例如: template<class T> class Temp{ 在這個(gè)模板中,我假設了T這個(gè)類(lèi)型是一個(gè)類(lèi),并且有一個(gè)print()方法(t.print())。我們在簡(jiǎn)介中的min模板中其實(shí)也作了同樣的假設,即假設T重載了'>'操作符。 因為語(yǔ)法檢查被延遲,編譯器看到這個(gè)模板的時(shí)候,并不去關(guān)心T這個(gè)類(lèi)型是否有print()方法,這些假設在模板被使用的時(shí)候才被編譯器檢查。只要定義中給出的類(lèi)型滿(mǎn)足假設,就可以通過(guò)編譯。 之所以說(shuō)“有一部分”語(yǔ)法檢查被延遲,是因為有些基本的語(yǔ)法還是被編譯器立即檢查的。只有那些與模板參數相關(guān)的檢查才會(huì )被推遲。如果你沒(méi)有寫(xiě)class結束后的分號,編譯器不會(huì )放過(guò)你的。 3.2 繼承模板類(lèi)可以與普通的類(lèi)一樣有基類(lèi),也同樣可以有派生類(lèi)。它的基類(lèi)和派生類(lèi)既可以是模板類(lèi),也可以不是模板類(lèi)。所有與繼承相關(guān)的特點(diǎn)模板類(lèi)也都具備。但仍然有一些值得注意的地方。 假設有如下類(lèi)關(guān)系: template<class T> class A{ ... }; 則aint和adouble并非A的派生類(lèi),甚至可以說(shuō)根本不存在A(yíng)這個(gè)類(lèi),只有A<int>和A<doubl>這兩個(gè)類(lèi)。這兩個(gè)類(lèi)沒(méi)有共同的基類(lèi),因此不能通過(guò)類(lèi)A來(lái)實(shí)現多態(tài)。如果希望對這兩個(gè)類(lèi)實(shí)現多態(tài),正確的類(lèi)層次應該是: class Abase {...}; template<class T> class A: public Abase {...}; 也就是說(shuō),在模板類(lèi)之上增加一個(gè)抽象的基類(lèi),注意,這個(gè)抽象基類(lèi)是一個(gè)普通類(lèi),而非模板。 再來(lái)看下面的類(lèi)關(guān)系: template<int i> class A{...}; 在這個(gè)情況下,模板參數是一個(gè)數值,而不是一個(gè)類(lèi)型。盡管如此,a10和a5仍然沒(méi)有共同基類(lèi)。這與用類(lèi)型作模板參數是一樣的。 3.3 靜態(tài)成員與上面例子類(lèi)似: template<class T> class A{ static char a_; }; 這里模板A中增加了一個(gè)靜態(tài)成員,那么要注意的是,對于aint1和adouble1,它們并沒(méi)有一個(gè)共同的靜態(tài)成員。而aint1與aint2有一個(gè)共同的靜態(tài)成員(對adouble1和adouble2也一樣)。 這個(gè)問(wèn)題實(shí)際上與繼承里面講到的問(wèn)題是一回事,關(guān)鍵要認識到aint與adouble分別是兩個(gè)不同類(lèi)的實(shí)例,而不是一個(gè)類(lèi)的兩個(gè)實(shí)例。認識到這一點(diǎn)后,很多類(lèi)似問(wèn)題都可以想通了。 3.4 模板類(lèi)的運用模板與類(lèi)繼承都可以讓代碼重用,都是對具體問(wèn)題的抽象過(guò)程。但是它們抽象的側重點(diǎn)不同,模板側重于對于算法的抽象,也就是說(shuō)如果你在解決一個(gè)問(wèn)題的時(shí)候,需要固定的step1 step2...,那么大概就可以抽象為模板。而如果一個(gè)問(wèn)題域中有很多相同的操作,但是這些操作并不能組成一個(gè)固定的序列,大概就可以用類(lèi)繼承來(lái)解決問(wèn)題。以我的水平還不足以在這么高的層次來(lái)清楚的解釋它們的不同,這段話(huà)僅供參考吧。 模板類(lèi)的運用方式,更多情況是直接使用,而不是作為基類(lèi)。例如人們在使用STL提供的模板時(shí),通常直接使用,而不需要從模板庫中提供的模板再派生自己的類(lèi)。這不是絕對的,我覺(jué)得這也是模板與類(lèi)繼承之間的以點(diǎn)兒區別,模板雖然也是抽象的東西,但是它往往不需要通過(guò)派生來(lái)具體化。 在設計模式[4]中,提到了一個(gè)模板方法模式,這個(gè)模式的核心就是對算法的抽象,也就是對固定操作序列的抽象。雖然不一定要用C++的模板來(lái)實(shí)現,但是它反映的思想是與C++模板一致的。 4. 參考資料[1] 深度C++對象模型,Stanley B.Lippman, 侯捷譯 [2] Thinking In C++ 2nd Edition Volumn 1, Bruce Eckel [3] 定義-- 英文為definition,意思是"Make this variable here",參見(jiàn)[2] p93 [4] Design Patterns - Elements of Reusable Object-Oriented Software GOF |
聯(lián)系客服