一、为什么会引入模板?
1.1 简化代码
举个栗子~
我们写个交换函数:
// 1.交换两个int类型的数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
// 2.交换两个double类型的数
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
// 3.交换两个char类型的数
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
这三个函数的代码逻辑都是一样的,仅仅是函数的形参类型不同。
那我们是否可以将三个函数写成一个,看起来不那么冗余呢?当然可以,这就引入了模板。
template<class T>
void swap(T& x1, T& x2)
{
T x = x1;
x1 = x2;
x2 = x;
}
别看这只是简化了两组代码,想想对于stack的实现,我们总不可能写一个int类型的,再写一个double类型,再写一个char类型的吧,使用上模板就大大优化了代码。
1.2 模板的使用规范
通过这样的模板定义,可以创建泛型的类或函数,使用时可以将具体的类型作为实参传递给模板参数。例如,在类模板的实例化时,可以将T替换为具体的类型,比如int或double。
template<class T> 是定义一个类模板或函数模板的语法。在这里,<class T> 是模板参数列表,指定了模板中使用的类型参数。
template:关键字表示接下来是一个模板定义。
<class T>:尖括号内是模板参数列表(可以有多个模板参数),用于指定模板中使用的类型参数。在这个例子中,使用了一个类型参数T。
class:关键字class 在这里用来声明一个类型参数,并且可以用于表示任何类型(包括内置类型、用户自定义类型等)。
T:这里的T是一个类型参数的名字,可以在模板中作为类型的占位符使用。
模板原理:
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对字符类型也是如此。
二、模板种类
C++中有两种主要的模板:函数模板和类模板。
2.1 函数模板
(1)函数模板是一种可以生成一组相关函数的模板,这些函数在形式上相同但可以处理不同类型的数据。
(2)函数模板的定义以关键字template开始,后面跟着模板参数列表和函数定义。
(3)模板参数可以是类型参数、非类型参数和模板参数包。
之前的swap函数用的就是函数模板。
template<class T>
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
2.2 类模板
(1)类模板是一种可以生成一组相关类的模板,这些类在形式上相同但可以处理不同类型的数据。
(2)类模板的定义以关键字template开始,后面跟着模板参数列表和类定义。
(3)模板参数可以是类型参数、非类型参数和模板参数包。
template<class T>
class Stack
{
private:
vector<T> elements;
public:
void push(const T& element)
{
elements.push_back(element);
}
T pop()
{
T element = elements.back();
elements.pop_back();
return element;
}
};
int main()
{
Stack<int> stack; // 实例化为 Stack<int>
// 使用模板时,可以根据具体的类型进行实例化,编译器会根据模板定义生成相应的代码。
stack.push(10);
int element = stack.pop();
cout << element << endl;
return 0;
}
三、类型参数与非类型参数
3.1 比较
类型参数指的是在模板中使用的类型占位符。使用class或typename关键字声明类型参数,并用标识符标记。
template <class T>
非类型参数:非类型参数指的是在模板中使用的非类型的值参数。非类型参数可以是整数、指针、引用、枚举、对象等。通过在模板参数列表中使用合适的类型声明和标识符,可以将其作为模板的参数。
template <int N>
具体例子:
template<class T,int N> // T是一个类型占位符,用来接收一个类型;N不是类型,是一个对象。
class Array
{
public:
// N是常量,不能修改
Array()
{
// N = 10;
// vector是一个可以改变size的顺序容器
// Array是一个定长容器
}
private:
T _a[N];
};
3.2注意
(1)非类型模板参数基本是整型,也可以是 /short/char/long/long long
(2)浮点数,类对象以及字符串是不允许作为非类型模板参数的。
(3)非类型的模板参数必须在编译期就能确认结果。
(4)自定义类型不能做非类型模板参数。
四、模板的特化
特化是针对某些类型的特殊化处理,当我们定义一个模板时,它会用通用的方式处理不同类型或值的输入。但有时我们需要为特定的类型或值提供定制化的实现。这就是模板特化的作用。
模板的特化分有类型特化和非类型特化。
4.1 类型特化
类型特化允许我们为特定的类型提供独立的实现方式。
使用template <>语法来定义类型特化。在尖括号中指定特化的类型。
在特化实现中,我们可以重新定义类的成员、方法或特定的行为,以满足特定类型的需求。
下面举个例子:
// 1.原版本
template <class T>
void sort(T arr[], int size)
{
}
// 2.特化版本
template <>
void sort<double>(double arr[], int size)
{
// 特化版本的实现
}
4.2 非类型特化
非类型特化允许我们为特定的非类型参数提供定制的实现方式。
非类型参数可以是整数、指针、引用等,通过特化可以为不同的参数值提供不同的实现。
使用template <>并在模板参数列表中指定非类型参数的值来定义非类型特化。
在非类型特化实现中,我们可以对特定的非类型参数进行特定操作或定义特殊行为。
例如,如果我们有一个通用的模板类template <int N> class Array,我们可以为不同的数组大小提供特化版本:
// 1.原版本
template <int N>
class Array
{
}
// 2.特化版本
template <> class Array<10>
{
// 特化版本针对大小为10的数组
};
小结:模板特化增强了C++的灵活性和重用性。它允许我们根据特定类型或值的要求,提供个性化的实现。通过使用类型特化和非类型特化,我们可以为不同的类型参数或非类型参数提供特定的行为,满足特定需求,并在代码中获得更好的性能和效果。
4.3 全特化与偏特化
特化分为 全特化:全部的参数都特化
偏特化:可以是特化部分参数/或者对参数的进一步限制
// 1.原模板
template<class T1, class T2>
class Data
{
public:
Data() { cout << "原模板类:Date<T1,T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 2.全特化
template<>
class Data<int, char>
{
public:
Data() { cout << "全特化:Date<int,char>" << endl; }
private:
};
// 3.偏特化
template<class T2>
class Data<int, T2>
{
public:
Data() { cout << "偏特化:Date<int,T2>" << endl; }
private:
};
// 指针的偏特化
template<class T1,class T2>
class Data<T1*, T2*>
{
public:
Data() { cout << "偏特化:Date<T1*,T2*>" << endl; }
private:
};
// 引用的偏特化
template<class T1,class T2>
class Data<T1&, T2&>
{
public:
Data() { cout << "偏特化:Date<T1&,T2&>" << endl; }
private:
};