C++模板
前言
在C语言中,当我们的需求不同时,我们需要书写参数类型不同但功能相同的函数去实现,但是这样的代码书写方式会造成代码的冗余性过高。
例如:一个实现求和功能函数,当需求是对两个整形求和时我们需要写一个参数为整形的求和函数,当需求变为对两个浮点型数据求和时,我们又要重新写一个参数为浮点型的求和函数,其实这样代码的复用性并不高。
而在C++中, 我们可以利用函数的重载来满足对不同参数类型的数据的需求,但是本质的问题没有解决,代码的复用性太低。由此,C++为我们提供了一种全新的方式——模板。
函数模板
概念:
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
语法:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
typename是用来定义模板参数关键字,也可以使用class,但是切记不能使用struct。
例如,一个交换函数的函数模板
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
函数模板的意义就在于类型也能被参数化了。
模板的实例化:生成模板函数
模板的实例化是指在函数的调用点,编译器用用户指定的类型,从原模版实例化一份函数代码出来。
例如:
//函数模板
template<typename T>//定义一个模板参数列表
bool compare(T a, T b)//compare是一个函数模板
{
return a > b;
}
int main()
{
//函数的调用点:要进行模板的实例化
//模板名+参数列表==>从函数模板实例化一份函数代码
compare<int>(10, 20);
//上述调用函数针对int类型参数实例化后生成的模板函数
//bool compare<int>(int a,int b)
//{
// return a>b;
//}
compare<double>(10.5, 20.5);//函数的调用点
//上述调用函数针对double类型参数实例化后生成的模板函数
//bool compare<double>(double a, double b)
//{
// return a > b;
//}
return 0;
}
注意:
- 模板是不参与编译的,因为在未调用函数模板时,并不清楚类型。只有在调用函数模板时,才会根据具体的类型进行实例化。
- 一种参数类型只实例化一次,后续直接使用实例化后的模板函数代码即可。
函数模板的实参推演:
函数模板的实参推演是指,当用户为指明类型时,可以根据用户传入的实参类型,来推导出模板类型参数的具体类型。
例如:
//函数模板
template<typename T>//定义一个模板参数列表
bool compare(T a, T b)//compare是一个函数模板,函数模板体是不进行编译的
{
cout << "compare template" << endl;
return a > b;
}
int main()
{
//未明确指明实参类型时,根据传入的实参类型int推导出模板类型参数的类型int
compare(20, 30);
}
问题:
当我们的需求为求一个整形和一个浮点型的数据较大值时,在该函数模板下编译器是无法进行推演的。我们的解决办法为:
- 将函数模板参数类型多定义一个,函数模板参数用两个模板类型参数定义。
template<typename T,typename Y>//定义一个模板参数列表
bool compare(T a, Y b)//compare是一个函数模板,函数模板体是不进行编译的
{
cout << "compare template" << endl;
return a > b;
}
int main()
{
//此时,实例化出的模板函数为:
//bool compare<int,double>(int a,double b)
//{
// return a>b;
//}
compare(int a,double b);
return 0;
}
- 在调用点进行强制类型转换。
int main()
{
//此时回将double类型的3.5强制类型转换为int
compare<int>(3,3.5);
}
但是对于某些类型来说,依赖编译器默认实例化的模板代码,代码处理的逻辑是错误的。
例如:
template<typename T>//定义一个模板参数列表
bool compare(T a, T b)//compare是一个函数模板,函数模板体是不进行编译的
{
cout << "compare template" << endl;
return a > b;
}
int main()
{
//根据实参推演得到的模板实参类型为:const char*
//得到的模板函数:
//bool compare<const char*>(const char* a, const char* b)
//{
// return a > b;
//}
compare("aaa","bbb");
return 0;
}
很明显可以看出,根据传入的实参类型推演出来的模板函数的功能是在比较两个常量字符串地址的大小,而我们的需求是要比较两个常量字符串的大小,很显然这样的是无法满足的。由此,我们引出了我们的模板的特例化(专用化)的概念。
模板的特例化:
模板的特例化是指:特殊(不是编译器提供的,而是用户提供的)的实例化。
例如:
针对compare函数模板,提供const char*类型的特例化版本。
template<typename T>//定义一个模板参数列表
bool compare(T a, T b)//compare是一个函数模板,函数模板体是不进行编译的
{
cout << "compare template" << endl;
return a > b;
}
template<>
bool compare<const char*>(const char* a, const char* b)
{
cout << "compare<const char*>" << endl;
return strcmp(a, b) > 0;
}
int main()
{
//根据实参推演得到的模板实参类型为:const char*
//得到的模板函数:
//bool compare<const char*>(const char* a, const char* b)
//{
// return a > b;
//}
return strcmp(a, b) > 0;
return 0;
}
在此我们需要注意,在定义针对某个类型的特例化版本时,不能省略template<>
,因为我们是根据上面的函数模板推演出来的,而<>
中不添加内容是因为函数模板进行参数推演时已经告诉编译器了。这样我们就可以实现两个常量字符串的比较了。
模板的非类型参数
模板的非类型参数是指在函数模板的定义中加入内置类型的参数,但是要求是在调用该函数模板进行实例化时,传入的参数必须整数类型(整数或者地址/引用),而且只能使用,不能修改。
例如:
template<typename T, int SIZE>//该例中int SIZE就是一个非类型参数,只能传常量
void sort(T* arr)
{
for (int i = 0; i < SIZE - 1; ++i)
{
for (int j = 0; j < SIZE - 1 - i; ++j)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 12,5,7,89,32,21,35 };
const int size = sizeof(arr) / sizeof(arr[0]);
sort<int, size>(arr);//函数模板的实例化
for (int val : arr)
{
cout << val << " ";
}
cout << endl;
return 0;
}
注意:
- 函数模板、模版的特例化、非模板函数的重载之间的执行逻辑:
对于编译器来说,编译器会优先将上述例子中compare
处理成函数名,先去寻找其普通函数,当没有普通函数时,如果有特例化版本,则会先调用其特例化版本,最后,才会调用其函数模板。 - 函数模板不能在一个文件中定义,在另外一个文件中使用的。因为函数模板是不进行编译的,文件会生成UND符号,在链接时却找不到其定义。模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能进行正常的实例化,产生能够被编译器编译的代码。所以,模板代码都是放在头文件当中的,然后再源文件中直接#include包含。
类模板
类模板的定义格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
在此处,我们以使用类模板实现一个栈为例,介绍类模板的细节。
//类模板
template<typename T>
//template<typename T=int> //带默认类型参数的类模板
//实例化时可以不传参数类型
class SeqStack//模板名称
{
public:
//模板名+类型参数列表====》类名称
//类模板中,构造函数和析构函数可以省略其类型参数列表
//其他出现模板的地方都加上类型参数列表
SeqStack(int size = 10)
:_pstack(new T[size])
, _top(0)
, _size(size)
{ }
~SeqStack()
{
delete[]_pstack;
_pstack = nullptr;
}
SeqStack(const SeqStack<T>& stack)
:_top(stack._top)
, _size(stack._size)
{
_pstack = new T[_size];
//不要用memcopy进行拷贝:会发生浅拷贝
for (int i = 0; i < _top; ++i)
{
_pstack[i] = stack._pstack[i];
}
}
SeqStack<T>& operator=(const SeqStack<T>& stack)
{
if (this == &stack)
return *this;
delete[]_pstack;
_top = stack._top;
_size = stack._size;
_pstack = new T[_size];
for (int i = 0; i < _top; ++i)
{
_pstack[i] = stack._pstack[i];
}
return *this;
}
void push(const T& val);
void pop()
{
if (empty() == 0)
--_top;
}
T top()const//对于只读操作接口,将其写为const成员,因为const成员变量不能访问普通成员方法,但是const成员方法普通成员变量可以访问
{
if (empty())
throw "stack is empty!";//抛出异常也代表函数逻辑结束
return _pstack[_top - 1];
}
bool full()const { return _top == _size; }//栈满
bool empty()const { return _top == 0; }//栈空
private:
T* _pstack;
int _top;
int _size;
//顺序栈数组底层按2倍的方式扩容
void expand()
{
T* ptmp = new T[_size * 2];
for (int i = 0; i < _top; ++i)
{
ptmp[i] = _pstack[i];
}
delete[]_pstack;
_pstack = ptmp;
_size *= 2;
}
};
template<typename T>//上一个参数作用域结束,需要重新定义
void SeqStack<T>::push(const T& val)//入栈操作
{
if (full())
expand();
_pstack[_top++] = val;
}
int main()
{
//类模板选择性实例化:要用什么实例化什么
//实例化后会生成模板类
//即:class SeqStack<int>{ };
SeqStack<int>s1;
s1.push(20);
s1.push(78);
s1.push(32);
s1.push(15);
s1.pop();
cout << s1.top() << endl;
return 0;
}
注意:
- 类名=模板名+类型参数列表。
- 类模板中,构造函数和析构函数可以省略其类型参数列表,其他出现模板的地方都要加上类型参数列表。
- 可以定义带默认类型参数的类模板,在实例化时可不传参数类型。
- 类模板的实例化:类模板在实例化时具有选择性,要用什么才会实例化什么;类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
模板的优缺点:
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性。
缺点:
- 模板会导致代码膨胀问题,也会导致编译时间变长 。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。