一、为什么要使用模板
C语言中,定义一个函数来交换两个变量的值,如果变量的类型有多种,那么函数也必要定义多个:
1. void Swapi(int* p1, int* p2) 2. { 3. int temp = *p1; 4. *p1 = *p2; 5. *p2 = temp; 6. } 7. 8. void Swapd(double* p1, double* p2) 9. { 10. double temp = *p1; 11. *p1 = *p2; 12. *p2 = temp; 13. } 14. 15. void Swapc(char* p1, char* p2) 16. { 17. char temp = *p1; 18. *p1 = *p2; 19. *p2 = temp; 20. } 21. 22. int main() 23. { 24. int a = 1, b = 2; 25. Swapi(&a, &b); 26. 27. double c = 1.1, d = 2.2; 28. Swapd(&c, &d); 29. 30. char e = 'a', f = 'b'; 31. Swapc(&e, &f); 32. 33. return 0; 34. }
由于c语言函数名修饰规则直接使用函数名生成符号表,不允许多个同名函数同时存在,多个函数名必须不同,因此只能写多个相似的swap函数,如果其他类型的变量也要进行交换,又得重新定义其他函数。C++支持函数重载,如果参数类型不同,多个函数可以使用同一个函数名:
1. void Swap(int& p1, int& p2) 2. { 3. int temp = p1; 4. p1 = p2; 5. p2 = temp; 6. } 7. 8. void Swap(double& p1, double& p2) 9. { 10. double temp = p1; 11. p1 = p2; 12. p2 = temp; 13. } 14. 15. void Swap(char& p1, char& p2) 16. { 17. char temp = p1; 18. p1 = p2; 19. p2 = temp; 20. } 21. 22. int main() 23. { 24. int a = 1, b = 2; 25. Swap(a, b); 26. 27. double c = 1.1, d = 2.2; 28. Swap(c, d); 29. 30. char e = 'a', f = 'b'; 31. Swap(e, f); 32. 33. return 0; 34. }
但是如果其他类型的变量也要进行交换,又得重新定义不同参数类型的函数重载,
不断使用函数重载会有以下问题:
(1)重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
(2)代码的可维护性比较低,一个出错可能所有的重载均出错
能不能只写一个与类型无关的函数来适配所有参数类型呢?
比如给编译器一个模子,让编译器根据不同的类型用这个模子来生成代码。C++增加了泛型编程:编写与类型无关的通用代码,是代码复用的一种手段,模板是泛型编程的基础。
二、函数模板
1.函数模板概念
函数模板代表了一个函数家族,函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
2.函数模板格式
1. template<typename T1,typename T2,,typename T3,typename T4,……,typename Tn> 2. 返回值类型 函数名(参数列表){}
typename是用来定义模板参数关键字,也可以使用class(千万不能使用struct)。
有了模板以后,交换函数可以这样写:
1. #include<iostream> 2. using namespace std; 3. 4. //函数模板 5. template <typename T> 6. void Swap(T& t1, T& t2) 7. { 8. T temp = t1; 9. t1 = t2; 10. t2 = temp; 11. } 12. 13. int main() 14. { 15. int a = 1, b = 2; 16. Swap(a, b); 17. 18. double c = 1.1, d = 2.2; 19. Swap(c, d); 20. 21. return 0; 22. }
F10-调试-窗口-监视-F11,发现Swap(a, b);和Swap(c, d);都调用了 void Swap(T& t1, T& t2)函数:
3.函数模板原理
实际上Swap(a, b);和Swap(c, d); 调用的是一个函数还是两个函数呢?
F10-调试-窗口-反汇编,发现他们调用的两个不同的函数地址,也就是调用了两个不同的函数,说明经过编译器处理,进行了模板的实例化,方便调试:
原理:
如下图所示,模板不是函数,是编译器用来产生特定具体类型函数的模具,把本来应该是我们做的事交给了编译器:
编译器通过调用实参,去推演模板板的形参,就推出了T到底是double型、int型还是char型,并实例化生成3份代码。
预处理阶段推演出3份代码以后,中间的模板就不存在了,编译器把它转化成后面3个函数,再去调对应的这3个函数。
整个过程其实是我们自己偷了个懒,即本该由我们自己写这3个函数,但是我们不想重复写,就自己写了一个模板,编译器通过模板帮我们生成了对应的代码。
注意:如果有多个参数时,参数的类型不同就不能用模板,因为编译器不知道T到底要传给哪种类型。
比如,a是int型,c是double型,,模板参数只有一个T,T不知道要推演成int,还是推演成double:
Swap(a, c);//编译报错
模板参数还可以做返回值:
1. //函数模板 2. template <typename T> 3. T Add(T& t1, T& t2)//返回值类型是泛型 4. { 5. return t1 + t2; 6. } 7. 8. int main() 9. { 10. int a = 1, b = 2; 11. Add(a, b); 12. 13. double c = 1.1, d = 2.2; 14. Add(c, d); 15. 16. return 0; 17. }
4.函数模板的实例化
函数模板的实例化:用不同类型的参数使用函数模板。模板参数实例化分为隐式实例化和显式实例化。
(1)隐式实例化
隐式实例化就是让编译器根据实参推演函数模板参数的实际类型。如前所示,a和b同类型,都是int,编译器会根据Add函数传的实参推演模板参数的实际类型T:
1. #include<iostream> 2. using namespace std; 3. 4. //函数模板 5. template <typename T> 6. T Add(T& t1, T& t2)//返回值类型是泛型 7. { 8. return t1 + t2; 9. } 10. 11. int main() 12. { 13. int a = 1, b = 2; 14. Add(a, b); 15. 16. double c = 1.1, d = 2.2; 17. Add(c, d); 18. 19. return 0; 20. }
以上就是隐式实例化。
但是如何让下面代码也编译通过呢?
Add(a,c);
编译器不知道要将T推演成int还是推演成double,但是我们可以将参数进行强转,在编译器推演T的类型之前,将两个参数的类型强行转为同类型:
1. Add(a,(int)c); 2. Add((double)a, c);
但是编译不通过:
这是因为强转会发生隐式类型转换,c是double型,强转为int型,中间会产生一个int类型的临时变量,而临时变量具有常性,Add函数的参数t1和t2没有使用const修饰,会导致权限放大,这是不允许的,因此要将Add函数的参数类型加上const关键字进行修饰:
1. template <typename T> 2. T Add(const T& t1, const T& t2)//返回值类型是泛型 3. { 4. return t1 + t2; 5. } 6. 7. int main() 8. { 9. int a = 1, b = 2; 10. Add(a, b); 11. 12. double c = 1.1, d = 2.2; 13. Add(c, d); 14. 15. Add(a, (int)c); 16. Add((double)a, c); 17. 18. return 0; 19. }
为了让Add(a,c);编译通过,强转只是解决的一种方法,还有另外一种方法:显式实例化。
(2)显式实例化
显式实例化就是在函数名后的<>中指定模板参数的实际类型:
1. Add<int>(a, c);//a和c都将作为int型传给形参 2. Add<double>(a, c);//a和c都将作为double型传给形参
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功,编译器会报错。
(3)模板参数的匹配原则
①一个非模板函数可以和一个同名的函数模板同时存在,该函数模板还可以被实例化为这个非模板函数:
1. int Add(int t1, int t2)//返回值类型是泛型 2. { 3. return t1 + t2; 4. } 5. 6. template <typename T> 7. T Add(const T& t1, const T& t2)//返回值类型是泛型 8. { 9. return t1 + t2; 10. } 11. 12. int main() 13. { 14. int a = 1, b = 2; 15. Add(a, b); 16. 17. Add<int>(a, c); 18. 19. return 0; 20. }
F10-调试-窗口-反汇编:
发现Add(a,b)与非模板函数匹配,编译器不需要进行模板函数实例化:
发现Add(a,c)调用函数模板,模板函数进行了实例化,生成了同名非模板函数:
②对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板:
1. int Add(int t1, int t2)//返回值类型是泛型 2. { 3. return t1 + t2; 4. } 5. 6. template <typename T> 7. T Add(const T& t1, const T& t2)//返回值类型是泛型 8. { 9. return t1 + t2; 10. } 11. 12. int main() 13. { 14. 15. Add(1, 2); 16. Add(1, 2.0); 17. 18. return 0; 19. }
F10-调试-窗口-反汇编:
发现Add(1,2)与非函数模板匹配,直接调用现成的Add函数,省去了对模板参数类型T的推演,编译器不需要进行模板函数实例化:
发现Add(1,2.0)与非模板函数不匹配,编译器只能根据实参生成更加匹配的Add函数: