泛型编程、函数模板、类模板
目录
一、泛型编程
1.泛型编程提出背景
1.1.代码复用案例解析
案例1:实现一个交换函数,并对不同类型参数进行函数重载
(1)调试
(2)代码解析
①代码复用问题
②泛型编程的解决方案
③上面泛型Swap函数模版的优点
1.2.泛型编程提出背景
2.泛型编程、模版介绍
2.1.泛型编程概念
2.2.模板概念
2.3.模板的分类
二、函数模版
1.函数模板概念
2.函数模板格式介绍
2.1.函数模版格式的解析
(1)函数模版的两种写法
①写法1
②写法2
(2)函数模版的详细解析
(3)案例1:单个类型的函数模版——定义Swap函数模版
3.函数模板的原理
3.1.分析当使用相同的函数模板来处理不同类型的参数时,是否会实例化出相同的函数实体。
3.2.函数模板原理介绍
4.函数模板的实例化
4.1.函数模板实例化概念
4.2.隐式实例化:让编译器根据实参自动推断出模板参数的实际类型
4.2.1.隐式实例化介绍
(1)隐式实例化概念
(2)隐式实例化格式
4.2.2.隐式实例化案例
(1)案例1:错误的函数模板实例化
(2)案例2:类型转换
4.3.显式实例化:在函数名后的<>中指定模板参数的实际类型
4.3.1.显示实例化介绍
(1).显示实例化概念
(2)显式实例化案例
(3)显式实例化作用
(4)注意事项
5.模板参数的匹配原则
5.1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
5.1.1.分析同函数名的函数模板和普通函数可以同时存在而不发生编译报错的原因
(1)案例
(2)原因
5.1.2.同函数名的函数模版和普通函数同时存在时,隐式实例化和显试实例化分别如何调用?
(1)隐式实例化只会调用普通函数,而不调用函数模板
(2)显式实例化只会调用函数模板,而不调用普通函数
(3)总结
5.2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
(1)案例1
(2)案例2:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三、类模版
1.类模板介绍
1.1.类模板概念
1.2.类模板的语法
1.3.类模板的背景
1.4.类模板的作用
1.5.类模板的案例
案例:实现一个数组的类模板
2.类模板实例化介绍
2.1.类模板实例化概念
2.2.类模板显式实例化格式
(1)有了类模板后,如何区分类名和类型
(2)类名与类型在普通类和类模板中的区别
(3)类模板不能隐式实例化,只能显示实例化的原因
3.类模板的声明和定义分离
3.1.类模板的声明和定义必须在同一文件中
(1)案例:在头文件中声明,在源文件中定义,声明和定义不在同一文件中使得在编译时发生链接错误
(2)类模板的声明和定义必须在同一文件的原因
3.2.普通类和类模板声明与定义分离的区别
(1)普通类
(2)类模板
类模板的成员函数在类外部定义的规则
3.3.类模板的声明和定义在同一文件的几种写法
(1)写法1:在头文件中声明和定义,成员函数在类模板内部定义
(2)写法2:在头文件中声明和定义,成员函数在类模板中定义外部定义
4.以栈为例,typedef 类型重命名不能解决容器存储不同数据类型的问题
一、泛型编程
1.泛型编程提出背景
1.1.代码复用案例解析
案例1:实现一个交换函数,并对不同类型参数进行函数重载
#include<stdlib.h>
#include <iostream>
using namespace std;
//解析:
//1.当我们想写一个交换函数Swap时,针对不同的参数类型都需要编写函数重载。这样做会导致代码重复,
//因为每个重载函数的内部逻辑都是类似的,只是参数类型不同。为了解决这个问题,C++提供了模板机制来
//实现泛型编程。
//2.使用函数重载虽然可以实现,但是有一下几个不好的地方:重载的函数仅仅是类型不同,代码复用率比
//较低,只要有新类型出现时,就需要用户自己增加对应的函数; 同时代码的可维护性比较低,一个出错可
//能所有的重载均出错
//Swap的函数重载
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
double c = 1.1, d = 2.22;
Swap(c, d);
char e = 'a', f = 'b';
Swap(e, f);
return 0;
}
(1)调试
(2)代码解析
上面的C++代码示例展示了函数重载的使用,通过为不同类型的参数提供不同的函数实现,从而实现了对不同类型数据的交换。然而,这种方法存在代码重复的问题,因为对于每种数据类型,都需要编写几乎相同的代码。这就是C++提出泛型编程要解决的一个重要问题。
①代码复用问题
在上述代码中,Swap
函数被重载了三次,分别用于交换int
、double
和char
类型的值。这些函数的实现几乎是一样的,只是数据类型不同。如果需要为更多类型提供交换功能,就需要编写更多的重载函数,这导致了代码的重复和冗余。
②泛型编程的解决方案
C++通过模板机制提供了泛型编程的能力,可以解决上述代码复用的问题。使用模板,可以编写一个通用的Swap
函数,该函数可以用于任何类型的数据。下面是使用模板重写的Swap
函数:
#include<stdlib.h>
#include <iostream>
using namespace std;
//template:译为模板
//泛型编程 -- 模板
template<class T> //或者写成:template<typename T>
//解析:T是模板类型参数的名称。这个参数名称不一定叫T,也可以是其他标识符,如X、Y等。
void Swap(T& x, T& y) //类型参数T的具体类型是由调用时的实参类型决定的。
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b); //调用Swap模板函数,T被实例化为int
double c = 1.1, d = 2.22;
Swap(c, d); //调用Swap模板函数,T被实例化为double
char e = 'a', f = 'b';
Swap(e, f); //调用Swap模板函数,T被实例化为char
return 0;
}
③上面泛型Swap函数模版的优点
-
减少代码重复:只需要编写一个模板函数,就可以用于任何类型的数据,大大减少了代码的重复。
-
提高可维护性:如果需要修改交换逻辑,只需要更改模板函数,所有使用该模板的代码都会自动使用新的逻辑。
-
增加灵活性:模板函数可以用于任何类型,包括自定义类型,而无需为每种类型编写特定的重载函数。
-
编译时类型检查:模板在编译时进行实例化,保证了类型安全,避免了运行时类型错误。
通过这个例子,我们可以看到C++提出泛型编程是为了提供一种更高效、更灵活、更安全的代码复用机制,从而解决了传统函数重载带来的代码重复和维护困难的问题。
1.2.泛型编程提出背景
在C++中,泛型编程的提出背景与软件开发中的一些常见需求和挑战密切相关,主要包括以下几个方面:
(1)代码复用:在泛型编程出现之前,程序员经常需要为不同的数据类型编写相同的算法或数据结构,例如,排序算法可能需要为整数、浮点数和字符数组分别实现。这导致了大量的代码重复,增加了维护成本。泛型编程通过允许编写与数据类型无关的代码,提高了代码的复用性。
(2)类型安全:早期的一些编程语言中,为了实现代码复用,程序员可能会使用像void*这样的指针来处理不同类型的对象,但这牺牲了类型安全,因为编译器无法在编译时检查类型错误。泛型编程在编译时进行类型检查,从而提高了代码的类型安全性。
(3)算法的通用性:许多算法和数据结构的概念是通用的,它们不依赖于特定的数据类型。程序员希望能够以一种通用的方式表达这些概念,而不是针对特定的数据类型。
(4)总结:C++中的泛型编程是为了解决代码复用、提高类型安全性、增强算法的通用性以及提升开发效率等问题而提出的。它通过模板机制在编译时根据传入的类型参数生成特定类型的代码,从而实现了同一份代码在不同数据类型上的重用,同时保证了类型的安全性和算法的灵活性。这种编程范式极大地简化了编程模型,减少了代码冗余,并提高了软件的可维护性和可扩展性。
2.泛型编程、模版介绍
2.1.泛型编程概念
泛型编程是一种编程范式,它允许程序员编写与数据类型无关的代码,从而创建可重用的代码组件。泛型编程的核心思想是将算法和数据结构的通用概念抽象化为模板,这样这些模板就可以被用来处理多种数据类型,而不仅仅是单一特定的类型。泛型编程使得代码更加通用、灵活,并且易于维护。
以下是对泛型编程详细说明:
-
类型无关性:泛型编程关注于编写不依赖于特定数据类型的代码。这意味着相同的代码可以用于不同的数据类型,比如整数、浮点数、字符串等。
-
模板:在C++中,泛型编程通常是通过模板实现的。模板允许程序员定义一个框架,这个框架可以在编译时针对不同的数据类型进行实例化。
-
算法和数据结构的通用性:泛型编程鼓励开发通用的算法和数据结构,这些算法和数据结构可以应用于多种数据类型,而不是为每种类型编写特定的实现。
-
代码重用:泛型编程提高了代码的重用性,因为相同的代码可以用于不同的数据类型,减少了代码的重复。
2.2.模板概念
模板提供了一种方法来定义函数或类,使得这些函数或类可以处理任何类型的数据,而不仅仅是单一特定类型。模板的定义并不是函数或类的具体实现,而是一个蓝图或公式,在编译时模板用于生成针对特定类型的函数或类的具体实现。
注意:泛型编程是用模版实现的。
2.3.模板的分类
(1)函数模板: 函数模板允许编写一个函数,它可以操作任何类型的参数。编译器会根据传递给函数的参数类型自动生成函数的特定实例。
(2)类模板: 类模板允许定义一个可以处理任何类型的类。类模板成员函数通常在类模板外部定义。
二、函数模版
1.函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.函数模板格式介绍
2.1.函数模版格式的解析
(1)函数模版的两种写法
①写法1
//函数模版的定义格式:
template<typename T1, typename T2,......,typename Tn>//模板参数列表。模板参数列表
//用来声明一个或多个类型参数,这些类型参数在函数模板被调用时将被具体的类型所替换。
返回值类型 函数名(参数列表)
{
//函数体,这里编写函数的具体实现。
//函数体内部可以使用模板参数 T1, T2, ..., Tn来定义变量的类型。
}
②写法2
注意:我们常用写法2来实现函数模版。
//函数模版的定义格式:
template<class T1, class T2,......,class Tn>
返回值类型 函数名(参数列表)
{
//函数体,这里编写函数的具体实现。
}
(2)函数模版的详细解析
template<typename T1, typename T2, ..., typename Tn>
是声明一个函数模板的关键字和语法结构。注:class
和 typename
在模板参数列表中都可以用来定义类型参数,一定不可以使用struct定义类型参数,我们一般使用class定义类型参数。
-
template
:这是一个关键字,告诉编译器后面跟着的是一个模板的声明。模板是C++中实现泛型编程的一种机制,模板允许编写与类型无关的代码即这段代码可以适用于多种不同的数据类型,而不需要为每种类型单独编写代码。编译器根据模板和提供的实际类型生成特定的代码。 -
模板参数列表位于模板声明中的
template
关键字之后,并用尖括号<>
括起来。 -
typename
:这是另一个关键字,用于在模板参数列表中声明一个类型参数。在C++早期版本中也可以使用class
关键字来声明类型参数,但是typename
更准确地反映了参数是一个类型。 -
T1, T2, ..., Tn
:这些是类型参数的名称(注:一般使用大写),它们代表在函数模板定义中可以使用的任意类型。在函数模板实例化时,这些参数会被实际的类型所替换。T1
,T2
, …,Tn
是习惯上的命名,但实际上可以使用任何有效的标识符。 -
返回值类型
:这是函数模板返回的类型,它可以是任何类型,也可以是模板参数之一。 -
函数名
:这是模板函数的名称。函数名应该遵循C++标识符的命名规则,并且应该能够清楚地描述函数的目的。 -
参数列表
:这是函数模板的参数列表,参数的类型可以是模板参数(如T1
,T2
, …),也可以是具体的类型(如int
,double
等)。参数传递方式:参数可以按值传递,也可以按引用传递(如T1&
表示对T1
类型的引用),但是若可以使用传引用传递就用引用传递而不是值传递,因为引用可以减少拷贝。
(3)案例1:单个类型的函数模版——定义Swap函数模版
注意:C++库会提供一个swap函数
swap - C++ 参考 (cplusplus.com)
#include<stdlib.h>
#include <iostream>
using namespace std;
//template:译为模版
//泛型编程 -- 模板
template<class T>//或者写成:template<typename T>
//解析:T是模版类型的名称。这个类型名称不一定叫T,也可以叫X 或者 叫Y。
void Swap(T& x, T& y)//类型参数T的具体类型是由调用时的实参类型决定的。
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
double c = 1.1, d = 2.22;
Swap(c, d);
char e = 'a', f = 'b';
Swap(e, f);
return 0;
}
调试:
3.函数模板的原理
3.1.分析当使用相同的函数模板来处理不同类型的参数时,是否会实例化出相同的函数实体。
(1)解析:在上述代码中,我们定义了一个名为Swap
的函数模板,它的作用是交换两个相同类型的值。在main
函数里,我们三次调用了这个函数模板,每次调用都传入了不同类型的参数:Swap(a, b)
第一次是两个int
类型的变量,Swap(c, d)
第二次是两个double
类型的变量,ap(e, f)
第三次是两个char
类型的变量。
在C++中,编译器对函数模板的处理方式是:每当函数模板被调用时,如果模板参数是不同的类型,编译器就会根据这些类型生成一个特定的函数。因此,当我们分别用int
、double
和char
类型调用Swap
函数时,编译器为每种类型创建了一个独立的函数实例。
Swap(a, b)
:这里,T
被替换为int
,编译器生成了一个void Swap(int& x, int& y)
的函数。Swap(c, d)
:这里,T
被替换为double
,编译器生成了一个void Swap(double& x, double& y)
的函数。Swap(e, f)
:这里,T
被替换为char
,编译器生成了一个void Swap(char& x, char& y)
的函数。
(2)Swap(a, b)
、Swap(c, d)
、Swap(e, f)
这三个函数调用是否是同一个函数?
解答:Swap(a, b)
、Swap(c, d)
、Swap(e, f)
这三个函数调用并不是调用同一个函数。每个调用都会导致编译器根据传入的参数类型生成一个特定的函数版本。以下是详细的解析:
从汇编语言层面来看,由于每次调用Swap
函数时传入的参数类型不同,编译器通过函数模版实例化生成了具有特定参数类型的函数(例如Swap(int& x, int& y)
、Swap(double& x, double& y)
、Swap(char& x, char& y)
)。因此,在调试时,当我们使用Swap(a, b)
、Swap(c, d)
、Swap(e, f)
进行函数调用时,虽然看起来我们是在调用同一个Swap
函数,但实际上在计算机内部,它们分别调用了三个完全不同的函数实例,每个实例都有不同的内存地址和对应的汇编指令集,这一点可以从汇编代码中的call
指令指向的地址不同来证实。
(3)注意事项
① 当我们调用Swap
函数模板时,例如Swap(a, b)
、Swap(c, d)
、Swap(e, f)
,编译器会为每种参数类型生成一个特定的函数版本。由于int
、double
和char
类型在内存中占用的空间大小不同,这些特定版本的函数在执行时创建的栈帧大小也可能不同。
② 在汇编语言中,同一个函数意味着它们具有相同的机器指令序列和在内存中的唯一地址。由于Swap
函数模板针对不同的类型参数生成了不同的函数版本,这些版本在汇编层面有不同的指令序列和地址。因此,Swap(int& x, int& y)
、Swap(double& x, double& y)
和Swap(char& x, char& y)
是三个不同的函数,它们在程序中的call
指令将指向不同的地址。
3.2.函数模板原理介绍
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
注意:函数模板实例化是指编译器在遇到函数模板调用时,根据传入的实际参数的类型来推断模板参数T的具体类型,并生成针对这些特定类型的函数代码的过程。
4.函数模板的实例化
4.1.函数模板实例化概念
(1)概念:在C++中,函数模板实例化是指编译器根据模板定义和传入的实际参数,生成具体函数实现的过程。函数模板是C++中的一种特性,允许编写与类型无关的代码,然后可以用不同的数据类型来调用这个模板,从而生成相应的函数版本。
总的来说,通过传参调用函数模板是触发函数模板实例化的过程,也可以理解为传参调用函数模版就是函数模板实例化。函数模板实例化分为两种类型:隐式实例化和显式实例化。
(2)注意事项
-
模板参数:函数模板定义中包含一个或多个模板参数(如
typename T
或class T
),模板参数T只是一个占位符这些参数在函数调用时会被具体的类型所替代。 -
函数模板实例化:是将函数模板转换为具体类型函数的过程,这个过程涉及到模板参数的具体化。注意:当我们多次使用相同类型的参数调用函数模板时,函数模板只会在第一次调用时被实例化。
-
实例化时机:模板实例化通常发生在编译时,而不是运行时。编译器在编译过程中检查模板调用,并生成相应的代码。
-
实例化结果:模板实例化产生的是一个具体的函数,它具有确定的类型参数,可以像普通函数一样被调用和优化。
4.2.隐式实例化:让编译器根据实参自动推断出模板参数的实际类型
4.2.1.隐式实例化介绍
(1)隐式实例化概念
当传参调用函数模板时,隐式实例化不需要程序员指定模板参数的具体类型。编译器会自动根据函数调用中的实际参数类型自动推断出模板参数的类型,并生成一个具体类型的函数版本。这个过程完全由编译器在编译时自动完成,无需程序员在代码中显式地指示编译器去做。
注意事项:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅。
(2)隐式实例化格式
template<class T>
T Add(const T& left, const T& right);
//隐式实例化调用示例
Add(a1, a2); //隐式实例化,编译器推导 T 为 int
Add(d1, d2); //隐式实例化,编译器推导 T 为 double
Add(a1, (int)d1);//隐式实例化:d1被强制转换为int后再传参,然后编译器会根据转换后的类型推导模板参
//数T。则编译器推导 T 为 int。
Add((double)a1, d1);//隐式实例化:a1被强制转换为double,然后编译器会根据转换后的类型推导模板参数
//T。则编译器推导 T 为 double。
4.2.2.隐式实例化案例
(1)案例1:错误的函数模板实例化
解决方式:
#include <string>
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
//Add(a1, d1);//该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,
//需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,
//但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者
//double类型而报错。注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,
//编译器就需要背黑锅
//此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);
return 0;
}
(2)案例2:类型转换
4.3.显式实例化:在函数名后的<>中指定模板参数的实际类型
4.3.1.显示实例化介绍
(1).显示实例化概念
①显式实例化是指程序员在代码中明确告诉编译器应该使用哪些具体类型来实例化一个函数模版或模板类。这与隐式实例化不同,在隐式实例化中,编译器会根据函数调用中的参数类型自动推断出模板参数的类型。
②在显式实例化的过程中,程序员通过在函数名后的尖括号<>
中指定模板参数的实际类型来指导编译器。这可以用于以下几种情况:
- 当编译器无法从函数调用中推断出模板参数类型时。
- 当程序员想要确保使用特定的类型实例化模板时。
- 当需要提高代码的可读性,明确指出模板实例化的类型时。
③显示实例化语法:传参调用函数模板时,在函数名后加上<类型>
,其中类型
是你想要用于实例化的具体类型。
(2)显式实例化案例
解析:
template<class T>
T Add(const T& left, const T& right);
//显式类型转换调用示例
Add<int>(a1, d1); //显式指定 T 为 int
Add<double>(a1, d1); //显式指定 T 为 double
//解析:Add<int>(a1, d1); 和 Add<double>(a1, d1); 是显式实例化的例子,这里明确指定了模板
//参数T的类型,即使实参a1和d1的类型不一致。在Add<int>(a1, d1);的情况下,d1会被从double转换
//为int,可能会丢失精度。
(3)显式实例化作用
- 明确类型: 显式指定模板参数类型,可以避免编译器推导错误或解决类型歧义。
- 类型转换: 即使实参类型不一致,显式实例化允许指定一个统一的类型,从而在调用时进行必要的类型转换。例如,
Add<double>(a1, d1)
会将a1从int
转换为double
。
(4)注意事项
显式实例化本身并不直接进行类型转换,而是指定了模板函数应该使用的具体类型。类型转换是在函数调用时发生的,而不是在显式实例化时。
类型转换:即使在函数调用时实参的类型与模板参数的类型不一致,显式实例化可以指定一个特定的模板参数类型,这样在调用函数时,编译器会自动将实参转换为模板参数指定的类型。例如,如果有一个模板函数Add
,通过显式实例化Add<double>
,在调用Add<double>(a1, d1)
时,如果a1
是int
类型而d1
是double
类型,编译器会将a1
从int
类型转换为double
类型,然后再调用Add
函数。
总的来说,如果函数调用中的实参类型与显式实例化指定的模板参数类型不匹配,编译器会尝试进行隐式类型转换换,如果无法转换成功编译器将会报错。
5.模板参数的匹配原则
5.1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
解析:同函数名的函数模板和普通函数可以同时存在,并且该函数模板可以被实例化为与普通函数相同类型的函数。
5.1.1.分析同函数名的函数模板和普通函数可以同时存在而不发生编译报错的原因
(1)案例
(2)原因
同函数名的普通函数和函数模版实例化之后生成的函数构成函数重载,而且普通函数和函数模版实例化之后生成的函数的函数命名规则不一样。
5.1.2.同函数名的函数模版和普通函数同时存在时,隐式实例化和显试实例化分别如何调用?
(1)隐式实例化只会调用普通函数,而不调用函数模板
原因:
- 精确匹配优先:在C++中,当调用一个函数时,编译器会优先选择与调用参数最匹配的函数。在这个例子中,
Add(1, 2);
直接对应于专门处理int
类型的非模板函数Add(int left, int right)
,这是一个精确匹配,因此编译器会选择这个函数而不是去实例化模板函数。 - 名称查找规则:在C++的名称查找阶段,非模板函数的名称会被先找到,因为它们在编译时就已经是确定的,不需要进行模板参数推导。如果存在一个与调用匹配的非模板函数,编译器会使用这个函数,而不是继续查找模板函数。这是因为C++的名称查找规则规定了非模板函数的优先级高于模板函数。这意味着在名称查找阶段,如果有一个非模板函数可以匹配当前的函数调用,那么编译器就会选择这个非模板函数,而不会考虑模板函数。
(2)显式实例化只会调用函数模板,而不调用普通函数
原因:
- 明确指示:通过使用显式实例化
Add<int>(1, 2);
,程序员明确指示编译器使用模板函数,并且指定了模板参数类型为int
。这告诉编译器忽略任何同名的非模板函数,即使非模板函数也能处理这个调用。 - 调用意图:显式实例化反映了程序员的意图,即使用模板版本而不是普通函数版本。因此,编译器会忽略普通函数,即使它们存在,也会实例化并调用模板函数。
(3)总结
总结来说,隐式实例化选择普通函数是因为它提供了最精确的匹配,而显式实例化选择模板函数是因为程序员明确要求编译器使用模板版本。
5.2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
(1)案例1
(2)案例2:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三、类模版
1.类模板介绍
1.1.类模板概念
在C++中,类模板是一种用于创建泛型类的方法,它允许程序员编写一个类定义,该定义不是针对任何特定的数据类型,而是可以用于任何数据类型。类模板提供了一种方式,可以创建出能够处理不同数据类型的类,而不必为每种数据类型编写单独的类代码。
1.2.类模板的语法
类模板的定义以关键字 template
开始,后跟一个模板参数列表,该列表用尖括号 <>
括起来。模板参数列表可以包含一个或多个由逗号分隔的类型参数。以下是类模板的基本语法:
template<class T1, class T2, ..., class Tn>
class ClassName//类模板名
{
//类成员变量和成员函数
};
这里的 T
是一个占位符名称,可以在类定义中使用它来表示任何类型。
1.3.类模板的背景
类模板的提出背景是为了解决代码复用的问题。在类模板出现之前,如果需要编写可以处理不同数据类型的类,程序员必须为每种数据类型编写几乎相同的代码,这导致了代码的冗余和维护困难。例如,如果要实现一个可以存储任何类型的数组类,那么就需要为 int
、float
、double
等每种类型分别编写一个数组类。
为了解决这个问题,C++引入了类模板的概念,使得可以用一种通用的方式来定义类,而不必关心具体的数据类型。
1.4.类模板的作用
- 代码复用:通过使用类模板,可以编写一次代码,然后用于多种数据类型,大大减少了代码量。
- 类型安全:类模板确保了在编译时类型错误会被检测出来,因为模板实例化时会检查类型的使用是否正确。
- 性能优化:由于模板类在编译时就已经确定了数据类型,编译器可以生成针对特定类型的优化代码。
1.5.类模板的案例
案例:实现一个数组的类模板
#include <cstring> //用于 memcpy
#include <cassert> //用于 assert
template <typename T>
class Array
{
private:
T* _arr;
size_t _size; //数组大小,即当前实际存储的元素个数
size_t _capacity; //数组容量,即数组当前可容纳的最大元素个数
public:
//构造函数
Array(size_t newcapacity = 4)
: _size(0), _capacity(newcapacity), _arr(nullptr)
{
_arr = new T[_capacity];
}
//(深拷贝)拷贝构造函数
Array(const Array& a)
: _size(a._size), _capacity(a._capacity), _arr(nullptr)
{
_arr = new T[_capacity];
std::memcpy(_arr, a._arr, a._size * sizeof(T));
}
//拷贝赋值运算符
Array& operator=(const Array& a)
{
if (this != &a)
{
T* tmp = new T[a._capacity];
std::memcpy(tmp, a._arr, a._size * sizeof(T));
delete[] _arr;
_arr = tmp;
_size = a._size;
_capacity = a._capacity;
}
return *this; // 传引用返回
}
//析构函数
~Array()
{
delete[] _arr;
_arr = nullptr;
_size = _capacity = 0;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
//下标运算符,添加越界检查
const T& operator[](size_t pos) const
{
assert(pos < _size);
return _arr[pos];
}
T& operator[](size_t pos)
{
assert(pos < _size);
return _arr[pos];
}
bool empty() const
{
return _size == 0;
}
// 其他成员函数…………
};
注意:在这个例子中,Array
类可以用于任何数据类型,如 Array<int>
、Array<double>
或 Array<std::string>
等。
2.类模板实例化介绍
注意:类模板提供了一种方式,使得我们可以创建出针对不同数据类型的类。当我们定义一个类模板时,我们并没有定义一个具体的类,而是定义了一个类的蓝图。只有当我们为这个蓝图指定了具体的数据类型时,编译器才会生成一个具体的类。
2.1.类模板实例化概念
类模板实例化是指使用具体的类型来替换类模板中的模板参数,从而生成一个具体的类的过程。这个过程是由编译器在编译时完成的。
注意:类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。类模板实例化必须只能显式实例化而不能隐式实例化。
2.2.类模板显式实例化格式
//类模板定义
//注意:在 C++ 语法中,当定义类模板时,模板参数列表中的类型参数是允许提供默认值的。当显式实
//例化这个类模板时,如果有模板参数带有默认值,对于这些带有默认值的参数,用户可以选择不提供
//具体类型。
template<class T1, class T2, ..., class Tn>
class ClassName//类模板名
{
//类成员变量和成员函数
};
//类模板显式实例化
ClassName<具体类型1, 具体类型2, ..., 具体类型n> 变量名(构造参数列表);
// 解析:
//1. 当定义的类模板的参数列表中的类型参数没有提供缺省值时,则我们在显式实例化类模板时,提供的
//具体类型参数个数必须与类模板定义中的类型参数个数相匹配,否则编译器会报错。
//2. 实例化类模板时,可以指定任意具体类型,包括但不限于基本数据类型(如int, double)和用
//户自定义类型(如类、结构体)。
//3. 变量名是您为显式实例化后的类实例所取的名称,它遵循C++变量命名规则。
//4. 构造参数列表仅在您需要调用非默认构造函数时提供。如果类模板定义了默认构造函数,并且您
//希望使用它,则可以省略构造参数列表。
(1)有了类模板后,如何区分类名和类型
在C++中,类模板是一个蓝图,用于生成具体的类。类模板本身不是类型,但是它可以来创建类型,即通过提供具体的模板参数来把类模板实例化为一个类型。以下是如何区分类名(即类模板名)、类型(即类模板实例化后创建的类型):
-
类模板名(类名):在C++中,类模板名通常用于指示这是一个类模板,而不是一个具体的类型。例如,
MyClass
是一个类模板名(类名)。 -
类模板实例化(类型):类模板实例化是通过将具体类型参数传递给模板来创建一个具体的类型。例如,
MyClass<int>
是一个类模板实例化(类型),它是一个具体的类型。 -
类型:一旦类模板被实例化,它就变成了一个具体的类型。例如,
MyClass<int>
变量名中的MyClass<int>
是一个类型,你可以用它来声明变量、创建对象等。
#include <iostream>
using namespace std;
//类模板定义
template <typename T>
class MyClass
{
T value;
public:
MyClass(T val)
: value(val)
{}
//其他成员函数...
};
//区分
//MyClass 是类模板名称(类名)
//MyClass<int> 是类型,由类模板实例化得到,可以用来声明变量或创建对象
//使用类型
MyClass<int> myIntObject; //声明一个 MyClass<int> 类型的对象
MyClass<double> myDoubleObject; //声明一个 MyClass<double> 类型的对象
//错误示例:不能直接使用类模板名称来声明对象
//MyClass myObject; //错误,因为 MyClass 是模板名称,不是类型
(2)类名与类型在普通类和类模板中的区别
在使用 class
定义类时,对于普通类而言,类名本身就代表一种类型。例如,定义 class MyClass {};
,这里的 MyClass
既是类名,也是类型
而对于类模板,类名和类型需要区分开来。类模板是一种通用的类定义,它可以根据不同的类型参数生成不同的具体类。以 template <class T> class Vector {};
为例,Vector
是类模板的类名,它本身并不是一个具体的类型;而 Vector<int>
是使用 int
作为类型参数对 Vector
模板进行实例化后得到的具体类型。
(3)类模板不能隐式实例化,只能显示实例化的原因
- 类模板显式实例化后,它变成了一个具体的类型,这个类型可以用来声明变量和创建对象。显式实例化是必要的,因为类模板在定义时不包含具体类型信息,只有在实例化时才会填充这些信息。
- 由于类模板的成员变量和成员函数中存在使用模板参数的情况,我们只有在显式实例化类模板后才能知道模板参数的具体类型,进而确定成员变量的大小,并最终确定整个对象的大小,以便在定义对象时为它分配正确的内存空间。
- 函数模板可以通过隐式实例化,在函数调用时通过实参推导出模板参数的具体类型。然而,类模板没有类似的推导机制,因为类模板实例化(即对象的创建)不涉及传递参数来推导类型。因此,类模板需要显式实例化来指定模板参数的具体类型。
3.类模板的声明和定义分离
3.1.类模板的声明和定义必须在同一文件中
(1)案例:在头文件中声明,在源文件中定义,声明和定义不在同一文件中使得在编译时发生链接错误
(2)类模板的声明和定义必须在同一文件的原因
C++编译器在编译时需要模板的定义来生成具体的模板实例。如果模板的定义不在声明所在的文件中,编译器在编译时可能无法找到模板的定义,从而无法实例化模板。这是因为模板不是像普通函数或类那样在编译时生成代码,而是根据模板参数在编译时生成。因此,模板的定义需要在使用模板的每个翻译单元中都是可见的。
3.2.普通类和类模板声明与定义分离的区别
(1)普通类
普通类可以将声明和定义分离,通常的做法是在头文件(.h
或 .hpp
)中进行类的声明,而在源文件(.cpp
)中进行类成员函数的定义。例如:
- 头文件
MyClass.h
class MyClass
{
public:
MyClass();
~MyClass();
void print();
};
- 源文件
MyClass.cpp
#include "MyClass.h"
#include <iostream>
MyClass::MyClass()
{
//构造函数实现
}
MyClass::~MyClass()
{
//析构函数实现
}
void MyClass::print()
{
std::cout << "Hello, World!" << std::endl;
}
(2)类模板
类模板同样可以进行声明和定义的分离,但与普通类不同的是,类模板的声明和定义必须放在同一个文件中。这是因为编译器在编译源文件时,需要知道模板会被实例化的具体类型才能生成对应的代码。如果将类模板的声明和定义分别放在 .h
和 .cpp
文件中,编译器在编译 .cpp
文件时,由于不知道模板会被实例化的具体类型,无法生成对应的代码,而在链接阶段又找不到对应的实例化代码,从而导致链接错误。
通常的做法是将类模板的声明和定义都放在头文件(.h
)中,并且一般上面写声明,下面写定义。例如:
- 头文件
Vector.h
//注:小写的vector是C++库中提供的顺序表。但是下面代码中的大写的Vector什么都不是,
//大写的Vector只是用来和小写的vector进行区分。
//动态顺序表
//注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public:
//构造函数
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
//自定义实现深拷贝构造函数
//…………
//析构函数
~Vector();//使用析构函数演示:在类中声明,在类外定义。
//插入函数
void PushBack(const T& data);
//删除函数
void PopBack();
//其他成员函数的声明
//…………
//统计栈存放有效数据个数
size_t Size()
{
return _size;
}
//返回动态数组下标为pos位置的T类型数据
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;//动态数组
size_t _size;//记录栈存放有效数据的个数
size_t _capacity;//记录栈最大容量
};
//注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if (_pData)
delete[] _pData;
_size = _capacity = 0;
}
template <class T>
void Vector<T>::PushBack(const T& data)
{}
template <class T>
void Vector<T>::PopBack()
{}
//其他成员函数的定义
//…………
类模板的成员函数在类外部定义的规则
当类模板的声明和定义分离时,在类外部定义成员函数之前必须加上模板参数列表。具体来说,要使用 template <class T>
(这里的 T
是类型参数,可根据实际情况使用其他标识符)来表明这是一个模板函数。同时,要指定成员函数所属的具体类域,即使用 Vector<T>::
来表示该成员函数属于 Vector
类模板以 T
为类型参数实例化后的类。例如,定义析构函数的语法为:
template <class T>
Vector<T>::~Vector()
{
//析构函数的具体实现
}
3.3.类模板的声明和定义在同一文件的几种写法
(1)写法1:在头文件中声明和定义,成员函数在类模板内部定义
注:在类模板内部定义成员函数时,成员函数无需带模板参数列表同时也不需要指定所在的类域。
(2)写法2:在头文件中声明和定义,成员函数在类模板中定义外部定义
注意:成员函数在类模板外部定义时,每个成员函数都必须带上模板参数列表,并且要使用域作用限定符‘::’来明确指定成员函数所属的类域。需要注意的是,这里的类域是指类模板通过实例化所生成的具体类型的类域,例如对于类模板Vector<T>,其类域表示为Vector<T>::。
4.以栈为例,typedef 类型重命名不能解决容器存储不同数据类型的问题
(1)案例:在设计栈类时,我们常常希望栈能够存储不同类型的数据。然而,使用 typedef
进行类型重命名在解决栈类存储不同类型数据的问题上存在一定局限性。
(2)typedef 类型重命名及局限性
typedef
可以为类型定义别名,在一定程度上方便代码编写,但不能完全解决栈类存储不同类型数据的问题。示例代码如下:
//typedef int STDataType;
typedef double STDataType;
class Stack
{
private:
STDataType* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack st1;
Stack st2;
//解析:用 typedef 对类型重命名后再用类 Stack 定义的栈后,栈 st1、栈 st2
//只能同时存放 int 或者 double 类型的数据,做不到让栈 st1 存放 int 类型的
//数据同时让栈 st2 存放 double 类型的数据。
return 0;
}
在上述代码中,使用 typedef
为栈存放的数据类型取别名后,通过 Stack
类定义的栈对象 st1
和 st2
只能存储同一种类型的数据(如上述代码中只能存储 double
类型),无法同时存储不同类型(如 int
和 double
)的数据。
(3)解决方式
①定义存放不同数据类型的栈类
通过分别定义不同数据类型的栈类,如 Stackint
和 Stackdouble
,可以让不同的栈对象存储不同类型的数据。示例代码如下:
class Stackint
{
private:
int* _a;
size_t _top;
size_t _capacity;
};
class Stackdouble
{
private:
double* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stackint st1; //栈 st1 存放 int 类型的数据
Stackdouble st2; //栈 st2 存放 double 类型的数据
return 0;
}
这种方式的缺点是代码冗余,当需要存储更多不同类型的数据时,需要定义更多的栈类。
②利用类模板解决
类模板是一种更灵活的解决方式,它可以根据不同的类型参数实例化出不同的类,从而实现栈类存储不同类型的数据。示例代码如下:
template<class T>
class Stack
{
public:
// 构造函数
Stack(int newcapacity = 4)
{
_a = new T[newcapacity]; //即使 new 失败了,我们也不需要检查动态空间是否申请失败,因为 new 失败会抛异常。
_top = 0;
_capacity = newcapacity;
}
// 析构函数
~Stack()
{
delete[] _a; //new[] 和 delete[] 要匹配使用来销毁指针 _a 指向的动态空间
_capacity = _top = 0;
_a = nullptr;
}
//其他成员函数…………
private:
T* _a;
size_t _top;
size_t _capacity;
};
int main()
{
//类模板的调用方式:类模板只能显示实例化调用
Stack<int> st1; //栈 st1 存放 int 类型的数据
Stack<double> st2; //栈 st2 存放 double 类型的数据
return 0;
}
在上述代码中,通过类模板 Stack
,可以根据不同的类型参数(如 int
和 double
)实例化出不同的栈类,从而让 st1
存储 int
类型的数据,st2
存储 double
类型的数据。这种方式避免了代码冗余,提高了代码的可复用性。