模板
- 前言
- 泛型编程
- 函数模板
- 概念
- 格式
- 函数模板的原理
- 函数模板的实例化
- 类模板
- 类模板的定义格式
- 类模板的实例化
前言
这篇博客讲的是模板的一些基本知识,并没有那么深入,但是如果你是为了过期末考试而搜的这篇博客,我觉得下面讲的是够了的。
之后会再出一篇深入讲解模板的博客。
泛型编程
先说一个例子:
如何实现一个通用的交换函数呢?
学了C++之后,我们有了函数重载这个东西。这是C不具有的语法。
我们可以通过函数重载来实现相同函数名交换不同类型的变量。像下面这样:
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实现的话,只能搞成不同函数名来实现不同变量的交换,而且还没有引用的语法。
但是不论是上面的函数重载还是用C来实现,都好麻烦,当我们每次想要交换新类型的交换的时候都要再重写一个函数。
也就是下面的缺点:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
活字印刷,相信大家都听过。
通过改变模具中的字来印刷出不同的文章。
那能否告诉编译器一个模具,让编译器根据不同的类型利用该模具来生成代码呢?
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同文字块(类型),来获得不同文字的文章(即生成具体类型的代码)。
这就是本篇博客要讲的模板。
目录中的泛型编程是指编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
模板分为两类,函数模板和类模板。
函数模板
概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式
//写函数前要加这个
template<typename T1, typename T2,......,typename Tn>
//typename后面的T1,这种就相当于我们定义变量时候的变量名,是
//随便取的,只不过这里是类型名。常用的还有K,V,跟T合起来就是KTV。
//然后再写函数
返回值类型 函数名(参数列表){}
来个交换的例子:
下面都是OK的。
typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)。
目前阶段,记住写成typename和class都是可以的。
再来个交换int和交换double的例子:
但是记住,上面的代码中,交换int和交换double走的不是一个函数。
函数模板的原理
大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生
产淘汰掉了很多手工产品。本质是什么,重复的工作交给了机器去完成。
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
如何证明呢?
看图(稍微懂一点反汇编就行):
右侧的反汇编中,交换int和交换double的函数地址是不一样的。
其实也可以对应到活字印刷术当中,我们想要有一份文章的拷贝,不是用活字印刷里面的木头块来读的,而是通过那些木头块来把文章印到纸上来读的。
所以上面的就很好理解了,不是用模板,而是用模板来实例化出对应的函数来使用。
其实,C++标准库中也是有交换的函数的,只不过用起来是小写swap。
可以看到,标准库中实现的就是用的模板。
我们可以直接使用:
函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
-
隐式实例化:让编译器根据实参推演模板参数的实际类型
先给个能跑的例子:编译器自动推演
再给个跑不了的例子:
出现上面问题的原因是a和d1的类型是不相同的,而模板函数中两个参数的类型是相同的,但是我们偏要生成一个int和double的函数,这样是生成不了的。因为编译器不能决定到底是用int还是用double来实现。编译器就犯难了,只能报错,不然就得背黑锅。 -
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
但是我们之前写的函数是可以进行隐式类型转换的。比如说下面这样:
如果想用模板来实现两个不同类型相加的时候,可用以下方法:
但是这样非常挫。
可以用下面的显示实例化来实现。
- 显示实例化:在函数名后的<>中指定模板参数的实际类型
这个方法在函数模板中不是那么常用,但是在类模板中就非常常用了。
再给个在函数模板中适用的场景。
上面不能直接使用func,因为模板中的T是不确定的。
这时候就要用到<>了。
当我们同时拥有模板和一个功能相同的加法函数的时候会发生什么?
会像上面这样,但是上面的写法是很挫的,不建议同时出现功能相同的函数模板。
类模板
类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
和函数模板很像。
类模板的是使用场景是什么呢?
我们来写一个简单的类模板。
如果想将类模板中的的成员函数在类外定义的话,得像这样:
还要说一点就是模板不支持分离编译。 声明放在.h 定义放在.cpp。
至于为什么的话放到后面深入讲模板的博客中说。
类模板的实例化
如果不指明T的类型,是会报错的。
那么用类模板的时候就要显示指出T的类型。
最后再给一个顺序表的例子:
直接把代码给出来:
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
:_data(nullptr)
,_top(0)
,_capacity(0)
{
if (capacity > 0)
{
_data = new T[capacity];
_top = 0;
_capacity = 4;
}
}
~Stack()
{
delete[] _data;
_data = nullptr;
_top = _capacity = 0;
}
void Push(const T& a)
{
if (_capacity == _top)
{
int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
T* newdata = new T[newcapacity];
if (_data)
{
memcpy(newdata, _data, sizeof(T) * _capacity);
delete[] _data;
}
_data = newdata;
_capacity = newcapacity;
}
_data[_top] = a;
++_top;
}
void Pop()
{
assert(_top);
--_top;
}
bool Empty()
{
return _top == 0;
}
const T& Top()
{
return _data[_top - 1];
}
private:
T* _data;
size_t _top;
size_t _capacity;
};
int main()
{
Stack<int> s;
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
s.Push(5);
while (!s.Empty())
{
cout << s.Top() << " ";
s.Pop();
}
cout << endl;
return 0;
}
其中最重要的是下面main函数中的Stack<int> s; 这条语句。
到此结束。。。