C++泛型编程——模板
文章目录
本章思维导图:
注:本章思维导图对应的xmind
和.png
文件都已同步导入至资源
1. 泛型编程的概念
在C++中,如果我们不借助库函数,要实现两个数据的交换函数swap
,由于要考虑到数据类型的多样性,我们难免要将swap
函数重载很多次,例如:
void swap(int& num1, int& num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
}
void swap(char& num1, char& num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
}
void swap(double& num1, double& num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
}
//…………………………
swap
函数的函数体基本相同,只有交换数据的类型不同,但就是由于这个小小的不同迫使我们产生很多冗余代码,使生产效率变得低下
为了解决这一问题,C++就支持了泛型编程这一概念:
允许我们用一个通用的代码模板来处理不同类型数据
在C++中,泛型编程就是靠模板来实现的
2. 模板
2.1 模板格式
基本格式为:
template<typename T1, typename T2,......,typename Tn>
template
为模板的关键字<>
里面的即模板参数,代表一个数据类型typename
可以用class
替代
2.2 函数模板
基本格式为:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
例如:
//定义一个函数模板swap
//T泛指所有类型
template <typename T>
void swap(T& num1, T& num2)
{
T temp = num1;
num1 = num2;
num2 = temp;
}
注意:
当模板函数的声明和定义不能分布在不同的文件
2.3 函数模板的实例化
用不同的类型使用函数模版生成一个具体的函数这一过程叫做函数模板的实例化,函数模板的实例化有以下两种方法:
2.3.1 隐式(推演)实例化
推演实例化——让编译器根据实参推演模板参数的实际类型
例如:
template <typename T>
void swap(T& num1, T& num2)
{
T temp = num1;
num1 = num2;
num2 = temp;
}
int main()
{
int a = 1, b = 2;
double a1 = 1.0, b1 = 2.0;
swap(a, b); //实例化函数模板为swap(int& num1, int& num2),并进行调用
swap(a1, b1); //实例化函数模板为swap(double& num1, double& num2),并进行调用
return 0;
}
需要注意:
如果该函数模板的模板参数只有一个,那么进行推演实例化时,传入的实参的类型就只能有一个(即模板不允许自动类型转换例如:
template <typename T> void swap(T& num1, T& num2) { T temp = num1; num1 = num2; num2 = temp; } int main() { int a = 1; double b = 2.0; swap(a, b); return 0; } //会报错: // “swap”: 未找到匹配的重载函数 // “void swap(T &,T &)”: 模板 参数“T”不明确
为了避免这个问题,一种解决方式就是增多模板参数:
template <typename T1, typename T2> void swap(T1& num1, T2& num2) { T1 temp = num1; num1 = num2; num2 = temp; } int main() { int a = 1; double b = 2.0; swap(a, b); return 0; }
一种方法是使用强制类型转换,使传入的类型相同。但这个方法也有一个需要注意的点:如果函数模板的形参为引用类型,但是没有被
const
修饰,那么就不能用强制类型转换来实现推演实例化:template <typename T> void swap(T& num1, T& num2) { T temp = num1; num1 = num2; num2 = temp; } int main() { int a = 1; double b = 2.0; swap(a, (int)b); //(int)b涉及数据类型的转换,因此产生了一个临时变量,而临时变量具有常性(const), //被const修饰的引用权限不能被放大,因此无法转换为没有被const修饰的形参T& num return 0; }
还有一种常用的方法就是使用显式实例化
2.3.2 显式实例化
显式实例化——在函数名后的<>中指定模板参数的实际类型
注意:使用显式实例化同样需要注意引用权限不能提升的问题
例如:
template <typename T>
T Add(const T& num1, const T& num2)
{
return num1 + num2;
}
int main()
{
int a1 = 1, b1 = 2;
double a2 = 2.0, b2 = 4.99;
int ret1 = Add<int>(a1, a2);
double ret2 = Add<double>(b1, b2);
return 0;
}
2.3 类模板
基本格式:
template<typename T1, typename T2,......,typename Tn>
class 类模板名 {};
例如:
//定义一个类模板stack
//该stack可以存储所有类型
template <class T>
class stack
{
public:
stack(int capacity = 3)
{
_a = new T[capacity];
_capacity = capacity;
_top = 0;
}
void push(T val) {}
//…………
private:
T* _a;
int _capacity;
int _top;
};
类模板只能显示实例化,其基本格式为:
类模板名 <数据类型> 对象名
例如对于上面的类模板stack
:
stack<int> st1;
stack<double> st2;
类成员函数声明和定义分离:
template <class T>
class stack
{
public:
stack(int capacity = 3)
{
_a = new T[capacity];
_capacity = capacity;
_top = 0;
}
void push(T val);
//…………
private:
T* _a;
int _capacity;
int _top;
};
//当声明和定义分离时,需要制定成员函数所在类的类型
//类模板的类模板名不是类名,类模板名<数据类型>才是类类型
//同样,成员函数的声明和定义不能分在两个不同的文件
template <class T>
void stack<T>::push(T val) {}
2.4 非类型模板参数
模板的参数列表支持传入非类型模板参数,以供在类模板或者函数模板的内部直接使用。
例如:
template<class T, size_t N = 10>
void func(T a)
{
cout << N << endl;
}
int main()
{
func(1);
return 0;
}
output:
10
这里需要注意:
- 非类型模板参数只能是整形数据,即
size_t, int, char
等类型 - 非类型模板参数必须在编译时就能确定结果,因此只能是常量
- 即非类型模板参数只能是整型常量
2.5 模板的特化
有小伙伴会问:什么是模板的特化?
我们先来看下面的代码:
template<class T>
bool Less(T a, T b)
{
return a < b;
}
int main()
{
string str1 = "a";
string str2 = "b";
string* ptr1 = &str1;
string* ptr2 = &str2;
cout << Less(str1, str2) << endl;
cout << Less(ptr1, ptr2) << endl;
return 0;
}
output:
1
0
-
我们定义了两个
string
对象:str1
“a”和str2
”b“,并用两个指针ptr1
和ptr2
分别指向这两个对象 -
我们调用函数模板
Less
欲比较对象str1
和str2
以及指针ptr1
和ptr2
所指向对象的大小 -
比较
str1
和str2
时,字符串“a”
确实小于字符串“b”
,也得到了正确的结果:1 -
比较
ptr1
和ptr2
时,确得到了相反的结果:0。这是为何?这是因为,我们传入函数模板的类型为string*
,是一个指针类型,因此函数Less
比较的也是两个指针的大小,而栈区的地址从高到低使用的,因此输出0
。
可见第二次比较与我们比较两指针指向的内容的本意不符,因此我们要对string*
这一特殊情况进行处理,也就是模板的特化。
简单点来说,模板特化就是:
原模板类的基础上,针对特殊类型所进行特殊化的实现方式
模板的特化分为函数模板的特化以及类模板的特化
2.5.1 函数模板的特化
在这里插入图片描述
要进行函数模板的特化,首先必须要有一个基础的函数模板
特化的基本格式为:
template
后面跟一个空的<>,即template<>
- 函数名后面跟一个<>,里面为特化的具体类型
- 函数的参数列表必须要和原函数模板的参数列表的格式一致
例如对于上面的Less
,我们对类型string*
进行特化:
template<class T>
bool Less(T a, T b)
{
return a < b;
}
template<>
bool Less<string*>(string* a, string* b)
{
return *a < *b; //比较的是指针指向的内容
}
事实上,为了提高代码的可读性,如果我们要对函数参数的某一特定类型进行特殊处理,我们为往往会以函数重载的方式解决:
bool Less(string* a, string* b)
{
return *a < *b;
}
因此,函数模板的特化实际上并不常用
2.5.2 类模板的特化
类模板的特化分为全特化和偏特化两种:
2.5.2.1 全特化
全特化即是将模板参数列表中所有的参数都确定化
格式为:
template
后面跟一个空的<>,即template<>
- 类模板名后面跟一个<>,里面为特化的具体类型
例如:
template<class T1, class T2>
class A
{
public:
void func()
{
cout << "A" << endl;
}
};
template<>
class A<int, int>
{
public:
void func()
{
cout << "A<int, int>" << endl;
}
};
int main()
{
A<int, int> a1;
A<double, int> a2;
a1.func();
a2.func();
return 0;
}
output:
A<int, int>
A
2.5.2.2 偏特化
全特化即是将模板参数列表中部分参数确定化
偏特化又分为两种,一种是确定部分参数的类型,一种是确定部分或所有参数的性质。我们分开来说明:
确定部分参数的类型
格式为:
template
后面跟一个<>,里面为不要被确定的模板参数- 类模板名后面跟一个<>,里面为不要被确定的模板参数以及被确定的具体类型
例如:
template<class T1, class T2> class A { public: void func() { cout << "A" << endl; } }; template<class T1> class A<T1, char> { public: void func() { cout << "A<T1, char>" << endl; } }; template<class T2> class A<double, T2> { public: void func() { cout << "A<double, T2>" << endl; } };
确定部分或全部参数的性质
这里的性质指的就是指针或者引用或者普通类型
格式为:
template
后面跟一个<>,里面的内容和原类模板一致- 类模板名后面跟一个<>,里面为确定的参数性质
例如:
template<class T> struct Less { bool operator() (T a, T b) { return a < b; } }; //只要传入的类型为指针类型,那么就会用这个模板 template <class T> struct Less<T*> { bool operator() (T* a, T* b) { return *a < *b; } };
3. 模板的本质
-
和类实例化对象类似,我们不能将类看成是一个具体的对象,它只是一个不占据空间的
蓝图
-
同样,我们也不能将函数模板和类模板看成是一个具体的函数和类,他们也只是实例化一个具体函数和类的
模具
-
只有我们使用一个或多个具体的数据类型用模板实例化时,才会形成一个具体的函数和类。
如上图所示, 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。因此实际上,我们没写的冗余代码
,编译器都替我们完成了,是编译器在替我们负重前行。