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;
}
......
以实现交换函数为例,在C语言中即使是近乎完全一致的的功能,通过代码实现,只要参数不同,我们就需要写对应类型的不同函数名的函数,在之前的学习中,我们已经学习了函数重载,我们不再需要起不同的函数名,比起C语言方便不少,但是不容忽视的是函数重载仍然有不好的地方。
1. 重载的函数仅仅是对应位置参数类型不同,绝大部分部分代码完全一致,即为了适应一两处类型的变化,我们就需要将整个函数近乎完全一致的重写,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数 2. 代码的可维护性比较低,函数重载的错误可能在运行时才被发现,而且错误信息可能不太容易理解,需要更多的调试工作才能确定问题所在,一个出错可能所有的重载均出错。
思考了函数重载仍然存在的问题,我们发现上述swap代码仅仅是某些地方的类型发生变化,实现逻辑不变,那么为了避免大量重复,我们是否能告诉编译器一个模子,等到具体使用时,再让编译器根据不同的类型利用该模子来生成代码呢?即我们如何实现一个通用的交换函数呢?
在C++中,就存在这样一个模具即模板,通过给这个模具中填充不同材料(类型),来获得不同 材料的铸件(即生成具体类型的代码)
模板的出现意义非常重大,模板是泛型编程的基础。模板的出现使得C++正式被业界作为不同于C语言的一门新语言所承认。模板可以分为函数模板与类模板。(有的地方也叫模板函数和模板类)
而泛型编程:是代码复用的一种手段,是指编写与类型无关的通用代码。
这样一份通用代码就可以应用于多种不同的数据类型。避免了为每种特定类型重复编写相似的代码,大大减少了开发工作量。 同时由于泛型编程在编译期进行类型检查,它可以确保代码在处理不同类型时的正确性。如果代码尝试对不兼容的类型进行操作,编译器会在编译期报错,而不是在运行时产生错误, 此外泛型编程使得代码更加灵活和可扩展。可以轻松地添加新的类型而无需修改现有的通用代码。只需要确保新类型满足通用代码的要求即可。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
template<class T1, class T2,......,class Tn> 返回值类型 函数名(参数列表){} 或template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表){}
//模版类型
template<class T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
typename是用来定义模板参数关键字,意为类型名称,也可以使用class(切记:不能使用struct代替class)
在早期我们是使用class来定义类型名称,后来才新增了typename关键字,typename与class在使用上并无区别。我们可以混着使用,当然实践中为了程序的可读性与整体观感,不推荐这么做。
template<class T1, typename T2>
void func(const T1& x, const T2& y)
{
//………………
}
<>叫做模板参数列表,<>类定义了模板参数,可以定义一个也可以定义多个,不同模板参数之间用逗号隔开,
注:模板参数只是用来一种表示,表明哪些地方之后编译器会替换为具体的类型,因此起什么名称都可以,只不过我们一般使用T(英文名type,即类型的意思)来命名。
template<class A, class B>
void func(const A& x, const B& y)
{
//………………
}
函数模板是一个蓝图,它本身并不是函数,是编译器用来产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,使用该函数时,我们明确使用该函数的参数类型,编译器再根据模板和确定的参数类型生成一份具体对应类型的函数,这样我们只写一份函数模板就可以用于不同的类型,重复的代码交由编译器实现,达到一种半自动化效果。
对于函数模板的使用,在编译器编译阶段,编译器需要根据传入的实参类型来推演生成对应 类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
用不同类型的参数使用函数模板,生成具体的函数,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
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, d2);//类型不同,程序报错。
Add(a1, (int)d2);//用户自己来强制转化
return 0;
}
隐式实例化是编译器根据实参推演模板参数的实际类型,因此,在上诉代码中我们只定义了一个模板参数T,我们传相同类型如int,double都没问题,但是当我们一个传int一个传double时,该调用语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型,通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。针对这种情况我们可以自己主动的将不同类型强制转换成相同;也可以进行显示实例化,这样编译器就知道T的需要推演的实际类型,编译器会主动将不同类型的参数进行类型转换。(显示实例化下文介绍。)
注意:处于安全、可读性、编译效率、歧义等考量,在模板中,编译器一般不会主动进行类型转换操作,显示实例化后,才会将不同类型进行转换。
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2);//定义多种模板类型之后,参数既可以是同一类型的也可以是不同类型的
Add(d1, d2);
return 0;
}
当然我们如过定义多个模板参数,那么在使用函数模板时参入相同或者不同的参数都是可以的
2. 显式实例化:在函数名后的<>中指定模板参数的实际类型
//用函数模版生成对应的函数 -> 模版的实例化
template<class t>
t Add(const t& left, const t& right)
{
return left + right;
}
template<class T>
T* func1(int n)
{
return new T[n];
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
// 推导实例化
cout << Add(a1, (int)d1) << endl;
cout << Add((double)a1, d1) << endl;
// 显示实例化
cout << Add<int>(a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
double* p1 = func1<double>(10);
return 0;
}
显式实例化是在函数名后加的<>中指定模板参数的实际类型,编译器直接根据<>中的类型确定参数类型,不在推到,如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
除此之外显示实例化还有一个场景是如func1函数,形参不是模板参数,但是函数内部跟返回值又用到模板参数,编译器无法根据参数推到,那我们就需要直接告诉编译器模板参数推导的类型
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这 个非模板函数,那么对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例(实例化就是在生成一个具体的函数,在已经有的情况下,这是一种浪费)。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板(如果使用已用的,类型转换会造成数据损失,并且有不必要的消耗)
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
}
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的
Add函数
}
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
template<class T1, class T2, ..., class Tn> class 类模板名 { // 类内成员定义 };
#include<iostream>
using namespace std;
// 类模版
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误,具体原因后面会讲
template<class T>
void Stack<T>::Push(const T& data)
{
// 扩容
_array[_size] = data;
++_size;
}
int main()
{
Stack<int> st1; // int
Stack<double> st2; // double
return 0;
}
类模板实例化与函数模板实例化不同,对于类模板,编译器需要在编译阶段就确定类的具体结构和成员函数的实现。这就要求明确指定模板参数的类型(具体原因较为复杂,本文深入讲解),类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
template<typename T>
class Stack
{
public:
Stack(int n = 4)
:_array(new T[n])
,_size(0)
,_capacity(n)
{}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
void Push(const T& x);
private:
T* _array;
size_t _capacity;
size_t _size;
};
int main()
{
// 类模板都是显示实例化
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double
Stack<int> st1; // int
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack<double> st2; // double
st2.Push(1.1);
st2.Push(1.1);
st2.Push(1.1);
Stack<double>* pst = new Stack<double>;
//...
delete pst;
return 0;
}
类模板的另一个函数的好处是,在C语言中,我们实现Stack时,改变数据结构存储的数据类是通过typedef控制的,我们无法同时typedef 不同内置类型为自定义类型。Stack一个项目中无法存储不同类型的数据。
但是通过类模板,我们可以实例化Stack<int>存储int类型的数据,Stack<double>存储double类型的数据,非常方便。
template<typename T>
class Stack
{
public:
Stack(int n = 4)
:_array(new T[n])
,_size(0)
,_capacity(n)
{}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
void Push(const T& x);
private:
T* _array;
size_t _capacity;
size_t _size;
};
template<class T>
void Stack<T>::Push(const T& x)
{
if (_size == _capacity)
{
T* tmp = new T[_capacity * 2];
memcpy(tmp, _array, sizeof(T) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
注:每个模板参数只能给当前的函数或类模板使用,如果类模板中成员函数声明和定义分离,我们需要重新定义模板参数,模板参数只是表示,方便后面编译器替换为具体的类型,名称不重要。
template<class X>//名称不重要,替换为X也行
void Stack<X>::Push(const X& x)
{
if (_size == _capacity)
{
X* tmp = new X[_capacity * 2];
memcpy(tmp, _array, sizeof(X) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
此外模板无法声明和定义分离,这个笔者后面会在模板进阶详细介绍,本文不再涉及。
类模板实例化时是按照需要实例化的,使用哪些成员函数就实例化哪些,没有使用的成员函数是不会实例化的。
即当我们用类模板实例化出一个类时,类会对成员函数进行扫描,确定有哪些成员函数,但是编译器不会对类的成员函数的实现细节进行细致检查,在我们写代码时程序不会显示错误,只有当我们调用对应的函数,编译器才会对相关函数细节进行细致检查,程序才会显示错误信息。
注:按需实例化并不会影响编译器对基本语法的错误检查。
namespace zlr
{
template<class T, class Container = vector<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top() const
{
return _con.func();//vector中没有实现这个函数
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
}
int main()
{
zlr::stack<int, vector<int>> st;
// 类模板实例化时,按需实例化,使用哪些成员函数就实例化哪些,不会全实例化
st.push(1);//push会进行实例化
st.push(2);
st.push(3);
st.push(4);
cout << st.top() << endl;//调用了,检查top内部细节,程序报错
st.pop();
return 0;
}
C++适配器会对容器的相关接口在进行转换,以上面代码为例,我们发现vector容器本生并没有实现func成员函数,但是编译器并没有报错,这是因为没有调用,编译器不会去详细检查调用成员函数的具体细节。(本例子仅为说明按需实例化问题,对相关容器、适配器等内容不了解可以先放一放)
但是需要注意的是按需实例化问题并不影响编译器对基本语法的检查,即使不调用,编译器依然可以检查出缺少标点,未知变量等基本语法错误。