【C++】-- 模板详解(一)

简介: 【C++】-- 模板详解

一、为什么要使用模板

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函数:

相关文章
|
1天前
|
算法 安全 编译器
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
|
1天前
|
JavaScript 前端开发 编译器
【C++初阶】C++模板编程入门:探索泛型编程的奥秘
【C++初阶】C++模板编程入门:探索泛型编程的奥秘
|
2天前
|
编译器 C++ 容器
【C++语言】模板(内附精美思维导图)
【C++语言】模板(内附精美思维导图)
|
12天前
|
存储 编译器 C++
6.C++模板(超全)
6.C++模板(超全)
|
18天前
|
编译器 C语言 C++
从C语言到C++_21(模板进阶+array)+相关笔试题(下)
从C语言到C++_21(模板进阶+array)+相关笔试题
24 2
|
18天前
|
编译器 C语言 C++
从C语言到C++_21(模板进阶+array)+相关笔试题(上)
从C语言到C++_21(模板进阶+array)+相关笔试题
24 0
|
22天前
|
存储 算法 C++
高效利用C++ STL库:标准模板库的使用技巧
本文介绍了C++ STL(标准模板库)的高效使用技巧,包括选择合适的容器类型、使用`emplace_back`而非`push_back`、预分配容器空间和范围for循环遍历容器。此外,还讨论了STL算法的运用,如用算法替代手动循环、使用lambda表达式和进行容器操作。通过这些技巧,开发者可以提升C++代码的性能和可读性。
|
22天前
|
程序员 编译器 C++
C++中的模板与泛型编程技术深度解析
C++中的模板与泛型编程技术深度解析
|
22天前
|
存储 算法 程序员
C++模板编程与泛型技术探秘
这篇文章探讨了C++中的模板编程和泛型技术,这两种技术增强了代码复用和抽象能力。文章介绍了函数模板和类模板的概念,通过示例展示了如何定义和使用它们。泛型技术是一种编程范式,强调编写与类型无关的代码,提高代码复用性和灵活性。C++11后的版本通过类型萃取和变长模板参数进一步扩展了模板功能。模板和泛型广泛应用在数据结构、算法、库和框架的开发中,如STL。掌握这些技术有助于编写更高效、灵活的代码,并推动软件开发的创新和进步。
|
25天前
|
编译器 C++
【C++】模板进阶 -- 详解
【C++】模板进阶 -- 详解