【C++】4-模板
1. 泛型
// 交换两个int型数据
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
// 交换两个double型数据
void Swap(double& left, double& right)
{
double tmp = left;
left = right;
right = tmp;
}
// 交换两个char型数据
void Swap(char& left, char& right)
{
char tmp = left;
left = right;
right = tmp;
}
根据我们现有知识,如果要交换两个相同类型变量的值,且这两个变量的类型可能是int、double、char,必须写3个交换函数,代价相对较大。
可不可以只写一个函数就能解决这个这一类交换的问题?——泛型
泛型编程:编写与类型无关的通用代码,可以代码复用。这种与类型无关的通用代码相当于模板,通过模板,可以复刻出多种不同类型的特定的代码。
2. 函数模板
2.1 概念
函数模板相当于一个函数家族,没有确定的参数类型,使用时要被参数化(传参),根据所传实参的类型确定特定的类型函数。
2.2 语法
template<typename T1, typename t2, ... ... , typename Tn>
返回值类型 函数名(参数列表){}
例:
template<typename T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
// typename是用来定义模板参数的关键字,也可以使用class(切记:不能使用struct代替class)
// 参数类型必须是T,与前面的typename一致。不能是auto,因为调用函数前要先为函数开辟栈帧,如果是auto,编译器就不知道为函数开辟多大的栈帧
// 这个函数模板对于任何类型的交换均支持
// 例
int main()
{
int a = 1, b = 2;
double c = 1.1, d = 1.2;
Swap(a, b);
Swap(c, d);
return 0;
}
2.3 原理
我们写一个模板给编译器,编译器根据这个模板产生特定类型函数。
上面代码调用过程如下:
编译器编译阶段,对于模板函数,编译器会根据传入的实参类型自动推演生成对应类型的函数以供调用,这称作函数模板的实例化。
例如:传入的实参类型int,编译器对实参类型分析,将T确定为int,然后生成一份专门处理int类型的代码。
-
如果调用两次int参数类型的Swap函数,编译器只会实例化出一个int参数类型的函数以供调用。
-
如果函数模板的参数类型只有一种,那么调用对应函数时所传参数的类型也应只有一种
template<typename T> void Swap(T& left, T& right) { T tmp = left; left = right; right = tmp; } int main() { int a = 1, b = 2; double c = 1.1, d = 1.2; Swap(a, b); Swap(c, a);// error C2782: “void Swap(T &,T &)”: 模板 参数“T”不明确 return 0; }
这里不会发生隐式类型转换(小->大,即int->double)
- 强制类型转换实际是发生在赋值操作,即调用函数过程,但是这里在模板函数的推演实例化过程就报错中止了,走不到调用函数那一步
- 即使推演实例化可以正常完成,调用函数传参时也会报错,因为函数模板的参数是引用数据类型,引用数据类型的传参本质上传的是一个临时变量(实参的拷贝),临时变量具有常属性,临时变量传给函数的形参,会发生传参时的权限放大,这是不允许的,除非在函数的形参那里也添加上const属性,但是不符合Swap函数的特征的。
2.4 函数模板的实例化
函数模板的实例化分为隐式实例化和显式实例化。前面所写代码都是隐式实例化。
-
隐式实例化:编译器根据实参类型推演模板参数的实际类型
template<typename T> T add(const T& left, const T& right) { return left + right; } int main() { int a = 1, b = 2; double c = 1.1, d = 2.2; cout << add(a, b) << endl; cout << add(c, d) << endl; cout << add(a, d) << endl;// 此处编译不通过 // error C2782: “T add(const T &,const T &)”: 模板 参数“T”不明确 // 解决方法: // 1.用户自己进行参数的显示类型转换--传参时,显式将参数类型转换为一致类型 cout << add((double)a, c) << endl; cout << add(a, (int)c) << endl; return 0; }
-
显示实例化:用户自己在函数名后的
<>
中指定模板参数的实际类型int main() { int a = 1, b = 2; double c = 1.1, d = 2.2; // 2. 显式实例化 -- 参数会发生隐式类型转换 cout << add<int>(a, c) << endl; cout << add<double>(a, c) << endl; return 0; }
2.5 函数模板参数
2.5.1 单参数
只有一个参数的函数模板。(前文已经介绍过了)
2.5.2 多参数
函数模板有不止一个参数。
template<typename T1, typename T2>
T1 add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a = 1, b = 2;
double c = 1.1, d = 2.2;
// 不同类型参数,函数中会发生隐式类型转换,会报警告
cout << add(a, c) << endl;
cout << add(c, a) << endl;
// 相同类型参数
cout << add(a, b) << endl;
return 0;
}
2.5.3 模板参数的匹配
-
一个非模板函数可以和同名函数模板同时存在,且该函数模板依然可以被实例化为非模板函数
// 专门处理int的加法函数 int add(const int left, const int right) { return left + right; } // 通用加法函数 template<typename T> T add(const T& left, const T& right) { return left + right; } int main() { cout << add(1, 2) << endl;// 匹配调用非模板函数,编译器无需专门实例化模板函数 cout << add<int>(1, 2) << endl;// 匹配调用模板函数,编译器须专门实例化模板函数 return 0; }
-
若同时存在非模板函数和同名函数模板,如果其他条件相同,那么调用时会优先调用非模板函数,而不会专门实例化一个模板函数;如果编译器会实例化出一个匹配程度更高的模板函数,那么就会调用这个实例化出的模板函数。
// 专门处理int的加法函数 int add(const int left, const int right) { return left + right; } // 通用加法函数 template<typename T> T add(const T& left, const T& right) { return left + right; } int main() { add(1, 2);// 匹配调用非模板函数,编译器无需专门实例化模板函数 add(1, 1.1);// 模板函数会生成比非模板函数具有更加匹配的参数的函数版本,调用编译器生成的这个更加匹配的模板函数 return 0; }
3. 类模板
3.1 概念
概念类比函数模板。
3.2 语法
template<typename T1, typename T2, ... , typename Tn>
class 类名称
{};
例:
template<typename T>
class Stack
{
public:
Stack(int capacity)
: _capacity(capacity)
, _top(0)
{
_a = (T*)malloc(sizeof(T) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
void Push(const T& data)
{
/*
* 扩容等
*/
_a[_top++] = data;
}
void Pop();// 声明
private:
T* _a;
int _top;
int _capacity;
};
// 类模板中的函数若在类外定义,需要加模板参数列表和指定类域
template<typename T>
void Stack<T>::Pop()
{
// 定义
}
3.3 实例化
-
类模板一般没有推演的时机,而是直接实例化。
例如,上述Stack代码,构造函数传参传的是个数,而非所需要的数据类型的参数。
例:
// 使用
int main()
{
Stack<int> st1;
st1.Push(1);
Stack<double> st2;
st2.Push(1.1);
Stack<char> st3;
st3.Push('a');
// 上面三种Stack类型只是类模板相同,但它们本身不是同一类型
st1 = st2;// 报错
return 0;
}
`
3.3 实例化
-
类模板一般没有推演的时机,而是直接实例化。
例如,上述Stack代码,构造函数传参传的是个数,而非所需要的数据类型的参数。
例:
// 使用
int main()
{
Stack<int> st1;
st1.Push(1);
Stack<double> st2;
st2.Push(1.1);
Stack<char> st3;
st3.Push('a');
// 上面三种Stack类型只是类模板相同,但它们本身不是同一类型
st1 = st2;// 报错
return 0;
}