在C
语言中是针对具体的类型编程的,但是C++
解决了这样的问题。最典型的就是使用交换函数Swap()
的时候:
//C code
void SwapInt(int x, int y);
void SwapDouble(double x, double y);
//……
我们可以发现一个问题,只要类型不符合swap()
的参数,就需要写新的交换函数,让它的参数符合交换数据的类型。尽管在C++
中支持“引用”和“重载”大大提高了代码的可读性,但是依旧有一些重复性的本质问题没有解决。因此,C++
又增加了一个“模板”的语法,使得不同类型的数据都可以用同一个模板生成的swap()
函数,这种编程方法就是泛型编程,这种编程风格能适用大多的类型。
template <typename T1, typename T2, …, typename Tn>//其中typename可以换成class
函数返回值 函数名()
{...}
#include
using namespace std;
template <typename T>//“虚拟类型T”或“广泛类型T”
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
int main()
{
int i = 10;
int j = 20;
double x = 3.14;
double y = 8.9;
Swap(i, j);//1
cout << "i,j=" << i << " " << j << endl;
Swap(x, y);//2
cout << "x,y=" << x << " " << y << endl;
return 0;
}
我们可以看到这个虚拟类型T很像是函数参数,我们姑且可以叫“类型参数”,平时我们使用的参数姑且可以叫“数据参数”,也就是说,C++
不仅可以传递“数据参数”还可以传递“类型参数”。
补充:
C++
其实本身也有一个swap()
专门用于交换,底层就是采用函数模板的。
在使用模板的时候需要注意隐式类型转化:
#include
using namespace std;
template <typename T1, typename T2>//“虚拟类型T”或“广泛类型T”
T2 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
int a = 10;
double b = 1.23;
printf("%lf" ,Add(a, b));
return 0;
}
2.3.显式实例化
上述Add()
的问题还能通过显式实例化来解决,调用得语句如下:
#include
using namespace std;
template <typename T>//“虚拟类型T”或“广泛类型T”
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 10;
double b = 1.23;
Add<double>(a, b);//相当于类型也被手动传递过去了
return 0;
}
#include
using namespace std;
template <typename T>//“虚拟类型T”或“广泛类型T”
T* init(int n)
{
T* p = new T[n];
return p;
}
int main()
{
init<int>(10);//不用显式实例化是没有办法调用这个函数的
return 0;
}
有的时候,隐式实例化会导致无法调用,这里举一个例子:
#include
using namespace std;
template<class T>
T* Function(int n)
{
return new T[n];
}
int main()
{
Function(10);//这个函数没有办法调用
return 0;
}
如果模板和模板的其中一个实例同时存在,则会优先使用实例,下面代码您可以尝试在编译器中调试一下,看看代码的跳转逻辑:
#include
using namespace std;
template<typename T>
T Add(T x, T y)
{
return x + y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
int i = 2, j = 3;
double x = 5, y = 4;
cout << Add(i, j) << endl;
cout << Add(x, y) << endl;
return 0;
}
当然这种code
是不建议写出来的,这里仅仅是作为演示……
上述例子中1
和2
处使用的Swap()
是否一样呢?不一样!模板会先对参数进行推演然后进行实例化。这两个步骤都交给了编译器来操作,理论上是会增加编译器的一点负担(编译复杂度的提高),但合理使用的话其实还好。
为什么C
没有比较官方的数据结构和算法库呢?很大的原因就是因为写出来的库不具有泛化的特点,冗余度很大。
注意:
auto
不能使用在函数形参部分,不要和模板混淆了。
类模板的使用频率要比函数模板要高,类模板实际上是为了解决typedef
的问题的。
#include
using namespace std;
typedef int STDataType;
//这里的typedef有点泛型编的意思
//但不是真正的泛型编程
//没有办法交给编译器自动替换类型
//只能自己手动更改类型
class Stack
{
public:
Push(STDataType x)
{}
private:
STDataType* _data;
int _top;
int _capacity;
};
int main()
{
Stack s1;//存储int的栈
s1.Push(10);//这个倒是没有什么问题
Stack s2;//存储float的栈
s2.Push(10.2);//这里就出现问题了,没有办法存储多种类型
return 0;
}
因此我们需要使用类模板,类模板的基本格式如下:
template<class T1, class T2, ..., class Tn>//也可以写成typename
class Stack
{...}
//需要注意的是,类模板必须使用显式实例化,不能隐式推导
需要我们注意的是,在代码中使用类模板,一定要显式使用:
#include
using namespace std;
template<class T>//也可以写成typename
class Stack
{
public:
Stack(size_t capacity = 4)
:_data(nullptr), _top(0), _capacity(0)//最后一个是为了防止Stack传0的情况
{
if (capacity > 0)
{
_data = new T[capacity];
_capacity = capacity;
_top = 0;
}
}
private:
T* _data;
size_t _top;
size_t _capacity;
};
int main()
{
Stack<int> a(10);//1,显式使用
Stack<char> b(5);//2,显式使用
Stack<double> c(8);//3,显式使用
return 0;
}
使用类模板的时候也需要注意,上述例子中1、2、3
使用的也不是同一个类型。接下来让我们把上面得Stack
类写得更加完整一些:
#include
#include
using namespace std;
template<typename T>//也可以改typename成class
class Stack
{
public:
//2.构造函数
Stack(size_t capacity = 4)//默认在创建对象的时候开辟4个空间
:_data(nullptr), _top(0), _capacity(0)//初始化列表初始化
{
cout << "Stack(size_t capacity = 4)" << endl;//打印,表示调用了构造函数
if (capacity > 0)//小于0就无法创建一个栈
{
_data = new T[capacity];//创建了容量为capacity的数组
_capacity = capacity;
_top = 0;
}
else
{
cout << "栈容量不合法" << endl;
}
}
//3.析构函数
~Stack()
{
cout << "~Stack()" << endl;
delete[] _data;//释放资源
_capacity = _top = 0;
}
//4.入栈
void Push(const T& x)
{
if (_top == _capacity)//扩容机制
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
T* tmp = new T[newCapacity];
if (_data)//如果数组之前申请成功
{
memcpy(tmp, _data, sizeof(T) * _top);//复制一份
delete[] _data;
}
_data = tmp;
_capacity = newCapacity;
}
_data[_top] = x;
++_top;
}
//5.出栈
void Pop()
{
assert(_top > 0);
_top--;
}
//6.判栈
bool Empty()
{
return _top == 0;
}
//7.取栈顶
T& Top()//这里最好加上引用,这里还可以替代修改栈顶数据的功能,不过如果在最前面加上const就不行了,这具体看需求
{
assert(_top > 0);
return _data[_top - 1];
}
private:
//1.定义栈类的相关成员变量
T* _data;//存储栈数据的数组
size_t _top;//栈顶
size_t _capacity;//栈容量
};
int main()
{
//有关try和catch配合new的使用以后我们会提及
try//这里是申请空间成功的做法
{
Stack<int> a(10);
a.Push(1);
a.Push(2);
a.Push(3);
a.Push(4);
a.Push(5);
a.Push(6);
a.Top()++;
while (!a.Empty())
{
cout << a.Top() << " ";
a.Pop();
}
cout << endl;
Stack<float> a(10);
a.Push(1.1);
a.Push(2.2);
a.Push(3.3);
a.Push(4.4);
a.Push(5.5);
a.Push(6.6);
a.Top()++;
while (!a.Empty())
{
cout << a.Top() << " ";
a.Pop();
}
cout << endl;
}
catch (exception& e)//这里是申请空间失败的做法
{
cout << e.what() << endl;
}
return 0;
}
模板是不支持分离文件编译(这是指声明放在.h
,定义放在.cpp
,而不是指在一个文件内函数的声明和定义分离),如果您这样做了,编译会报错,最好是单独放在一个文件里,原因我们以后再提及。
不过可以先看看下面的code
,下面代码我们将类模板的定义和声明放在了一个文件中:
#include
#include
using namespace std;
template<class T>//也可以写成typename
class Stack
{
public:
Stack(size_t capacity = 4)
:_data(nullptr), _top(0), _capacity(0)
{
cout << "Stack(size_t capacity = 4)" << endl;
if (capacity > 0)
{
_data = new T[capacity];
_capacity = capacity;
_top = 0;
}
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _data;
_capacity = _top = 0;
}
void Push(const T& x);
void Pop()
{
assert(_top > 0);
_top--;
}
bool Empty()
{
return _top == 0;
}
T& Top()//这里最好加上引用,这里还可以替代修改栈顶数据的功能,不过如果在最前面加上const就不行了,这具体看需求
{
assert(_top > 0);
return _data[_top - 1];
}
private:
T* _data;
size_t _top;
size_t _capacity;
};
template<class T>//这里是声明一下模板参数
void Stack<T>::Push(const T& x)//类模板声明与定义分离(同一个文件下)
{
if (_top == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
T* tmp = new T[newCapacity];
if (_data)//如果栈不为空
{
memcpy(tmp, _data, sizeof(T) * _top);
delete[] _data;
}
_data = tmp;
_capacity = newCapacity;
}
_data[_top] = x;
++_top;
}
int main()
{
//有关try和catch配合new的使用以后我们会提及
try//这里是申请空间成功的做法
{
Stack<int> a(10);
a.Push(1);
a.Push(2);
a.Push(3);
a.Push(4);
a.Push(5);
a.Push(6);
a.Top()++;
while (!a.Empty())
{
cout << a.Top() << " ";
a.Pop();
}
cout << endl;
}
catch (exception& e)//这里是申请空间失败的做法
{
cout << e.what() << endl;
}
return 0;
}
类模板只需要放在一个单独的文件就可以(一般也是这么做的),由于这个文件比较特殊,既有声明又有定义。因此我们可以叫这个新的文件为.hpp
,代表“既有定义又有声明的意思”。
实际上,对于类模板来说,放在.h
文件和.hpp
文件都是可以的(推荐使用后者)。
补充:对于类模板来说,要分清楚两个概念:“类的类名、类的类型”。在上述代码中,
Stack
是类的类名,而Stack
是类的类型。在没学类模板之前,类的函数声明和定义分离时,函数定义需要使用“类的类名”和“作用域访问限定符”来确定是类内部的函数:
class Data { public: void Print(); }; void Data::Print() { //函数的具体定义 }
而学习和使用了类模板后,就会发现这里使用的不是“类的类名”而是“类的类型”:
template<typename T> class Data { public: void Print(); }; template<class T>//这里是单独声明一下模板参数 void Data<T>::Print() { //函数的具体定义 }
既然函数形参有缺省值,模板里的类型是否也有缺省值呢?答案是有的:
template <typename T = 缺省类型>
//后续要使用缺省类型可以如此调用:Stack<> sta;
还有一些关于模板的更加复杂的知识我们以后再来做详细解答……