考慮一個問題:寫一個通用的加法程式,如下:
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
……
我們得把每種情況列出來,顯然十分麻煩。
C++為我們提供了一種解決方式,叫
泛型程式設計
(編寫與類型無關的邏輯代碼,是代碼複用的一種手段)。
模闆是泛型程式設計的基礎。
模闆又分為函數模闆與類模闆,本文主要來了解函數模闆。
一、函數模闆
代表了一個函數家族,該函數與類型無關,在使用時被參數化,根據實參類型産生函數的特定類型版本。
模闆函數的格式 :
template<typename Param1, typename Param2,...,class Paramn>
傳回值類型 函數名(參數清單)
{ ... }
eg:
template<typename T>
T Add(T left, T right)
{
return left + right;
}
這裡typename也可以用class,但最好用typename。
二、執行個體化
上述函數模闆構造好,它是如何工作的呢?
如下:
注意:模闆會被編譯兩次
1、編譯階段,檢查模闆代碼本身,檢視是否出現文法錯誤,如:遺漏分号 ,但可能會忽略一些文法錯誤。
2、在執行個體化期間,檢查模闆代碼,檢視是否所有的調用都有效,如:執行個體化類型不支援某些函數調用
如果上述程式中left和right類型不同,執行個體化時,不會自動轉化為已有的執行個體。
eg:
cout << Add(1.1, 2) << endl;
是以我們要如上強制轉化或者顯性執行個體化。
三、模闆參數
1、模闆形參表使用<>括起來
2、和函數參數表一樣,跟多個參數時必須用逗号隔開,類型可以相同也可以不相同
3、定義模闆函數時模闆形參表不能為空
4、模闆形參可以是類型形參,也可以是非類型新參,類型形參跟在class和typename後
5、模闆類型形參可作為類型說明符用在模闆中的任何地方,與内置類型或自定義類型 使用方法完全相同,可用于指定函數形參類型、傳回值、局部變量和強制類型轉換
6、模闆形參表中,class和typename具有相同的含義,可以互換,使用typename更加直覺。但關鍵字typename是作為C++标準加入到C++中的,舊的編譯器可能不支援。
(一)類型參數
(1)我們在定義模闆時,關于它的形參需要注意它的作用域。
看如下代碼:
typedef int T;
template<typename T>
void test(T t)
{
cout << "Type t=" << typeid(t).name() << endl;
}
T g;
int main()
{
test(1);
cout << "Type g=" << typeid(g).name() << endl;
system("pause");
return 0;
}
運作結果為:
值得思考的是:變量t和g分别是哪個T定義的呢?
模闆形參名字隻能在模闆形參之後到模闆聲明或定義的末尾之間使用,遵循名字屏蔽規則
是以如圖所示:
(2)模闆形參的名字在同一模闆形參清單中隻能使用一次,且每個形參前必須加上關鍵字typename(class)
eg: template<typename T,typename T>
系統會提示重定義。
(二)非模闆類型形參
是模闆内部定義的常量,在需要常量表達式的時候,可以使用非模闆類型參數。
eg:
template<typename T, int N>
void FunTest(T(&_array)[N])//此處表示數組的引用
{
}
四、模闆函數重載
(1)函數模闆重載
模闆函數與重載是密切相關的。實際上,從函數模闆産生的相關函數都是同名的,是以C++編譯系統采用重載的解決方法調用相應函數。
函數模闆本身可以用多種方式重載,這需要提供其他函數模闆,指定不同參數的相同函數名。
例如,有以下程式:
template<class T> //函數模闆1
void display(T *arr, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
cout << *(arr + i) << " ";
}
cout << endl;
}
template<class T>
void display(T *arr, int i, int j) //函數模闆2
{
int k = 0;
for (k = i; k < j; k++)
{
cout << *(arr + k) << " ";
}
cout << endl;
}
上述程式中有兩個同名的函數display,他們的參數不同,構成承載。
結果如下:
函數模闆也可以用其他非模闆函數重載,例如,将上例程式函數模闆二改為:
void display(double *arr, int i, int j) //函數模闆2
{
int k = 0;
for (k = i; k < j; k++)
{
cout << *(arr + k) << " ";
}
cout << endl;
}
(2)函數調用的比對順序
我們來看一個例子:
template<class T>
T add(T x, T y)
{
cout << "模闆函數:";
return x + y;
}
int add(int x, int y)
{
cout << "int:";
return x + y;
}
int main()
{
int a = 1;
int b = 2;
double f1 = 1.1;
double f2 = 2.2;
cout << add(1, 2) << endl;
cout << add(f1, f2) << endl;
system("pause");
return 0;
}
如果調用模闆函數,會顯示字樣,觀察一下運作結果:
是以我們有必要知道一些規則:
1、一個非模闆函數可以和一個同名的函數模闆同時存在,而且該函數模闆還可以被執行個體化為這個非模闆函數。
2、對于非模闆函數和同名函數模闆,如果其他條件都相同,在調動時會優先調動非模闆函數而不會從該模闆産生出一個執行個體。如果模闆可以産生一個具有更好比對的函數, 那麼将選擇模闆。
3、顯式指定一個空的模闆實參清單,該文法告訴編譯器隻有模闆才能來比對這個調用, 而且所有的模闆參數都應該根據實參演繹出來。
4、模闆函數不允許自動類型轉換,但普通函數可以進行自動類型轉換
五、模闆函數特化
有時候并不總是能夠寫出對所有可能被執行個體化的類型都合适的模闆,在某些情況下,通用模闆定 義對于某個類型可能是完全錯誤的,或者不能編譯,或者做一些錯誤的事情。
模闆特化:就是在執行個體化模闆時,對特定類型的實參進行特殊處理,即執行個體化一個特殊的執行個體版本,
當以特化定義時的形參使用模闆時,将調用特化版本,模闆特化分為全特化和偏特化;
. 函數模闆的特化,隻能全特化。
模闆函數特化形式如下:
1、關鍵字template後面接一對空的尖括号<>
2、函數名後接模闆名和一對尖括号,尖括号中指定這個特化定義的模闆形參
3、函數形參表
4、函數體 template<> 傳回值 函數名<Type>(參數清單)
{
// 函數體
}
我們先來看一下字元串比較的函數模闆:
template<class T>
int compare( T p1, T p2)
{
if (p1 < p2)
return -1;
else if (p1>p2)
return 1;
return 0;
}
int main()
{
const char *s1 = "world";
const char *s2 = "hello";
cout << compare(s1, s2) << endl;
system("pause");
return 0;
}
應該傳回1,看一下結果:
這是為什麼呢?它直接比較的是p1,p2的位址。
我們可以用特化解決這種特殊情況:
template<class T>
int compare( T p1, T p2)
{
if (p1 < p2)
return -1;
else if (p1>p2)
return 1;
return 0;
}
template<>
int compare<const char *>(const char *const p1, const char *const p2)
{
return strcmp(p1, p2);
}
int main()
{
const char *s1 = "world";
const char *s2 = "hello";
char *s3 = "world";
char *s4 = "hello";
cout << compare(s1, s2) << endl;
cout << compare(s3, s4) << endl;
system("pause");
return 0;
}
結果:
看以看出與特化版本的參數清單完全比對會調用特化版本,不比對的話在編譯階段會通過模闆生成所需的函數。