前言
单纯地直接写一个普通函数适用性比较小,比如int compare(int a,int b)
,这个函数直接比较了整型变量a和b的大小。但是如果你有了新需求,想比较两个double
型数字的大小,那么之前我们讲过重载,很容易想到重载来满足这个需求。但是我们写代码当然追求省,毕竟懒是人生学习的动力。这种情况使用重载会造成不必要的代码冗余,毕竟参数都一样。那么这次来学习一下模板是如何节省代码的。
首先来看重载的情况:
bool compare(int a,int b)
{
return a>b;
}
bool compare(double a,double b)
{
return a>b;
}
int main()
{
compare(1,2);
compare(1.3,2.5);
return 0;
}
针对每一个新需求都写一份重载,浪费时间
函数模板
为了省力气,我们希望通过调用一次compare
函数,如果想用比较int
类型的compare
就用,或者想用比较double
类型的compare
就用。这里我们先用模板重构一下上面的代码。
templete<typename T> //函数模板在写出来的时候,声明+定义,这样出来的是一个纯正的模板,而不是函数。
bool compare(const T &a,const T &b)//真正要用的时候是需要函数模板实例化出来一个函数的
{
return a>b;
}
int main()
{
compare(1,2); //这样在函数调用点调用的时候,编译器会自动实例化一个函数。
//compare<int>(1,2); //并且,如果函数名后没有使用限定<int>的话,会进行模板实参推演来自动识别参数类型
compare(2.3,2.5);
return 0;
}
上面解释了模板实例化和模板实参推演的概念,而且可以看到,没写模板之前,假如又遇到需要比较int
和double
类型数据的大小还需要再写一份重载的代码,起码多了3行,N多字母。但是有了模板,就可以在上面的模板声明中稍作改动,这样写:
templete<typename T,typename W>
bool compare(const T &a,const W &b);
int main()
{
compare(1,2.4);
}
这样,只需要加一个typename W
,两个参数就支持不同数据类型了。
那么现在遇到了一个新需求,要求输入一个数据和一个整形数字10做大小比较。只需要在函数模板参数上加一个非模板参数int com=10
,如下:
templete<typename T,int com=10>
bool compare(const T &a)
{
return a>com;
}
一份好的模板可以省下很多功夫,这也是学习类库的前驱知识。但是这样做存在一个问题:
如果要求比较两个常量字符串的大小,当然这需要比较其字典序的先后。我们看用上面的思路来比较。但是上面的代码就变成了比较俩字符串地址的大小了,是不符合要求的。遇到这种情况,我们就需要用到模板特例化。
模板特例化
针对已有模板所不能解决的问题,首先是想着补充这个已有模板的功能,这个思路就是模板特例化。需要模板已有,对其进行补充。
比如上面已经有了一个模板compare
,这里直接对其进行补充:
templete<typename T>
bool compare(const T &a,const T &b); //先声明好
templete<> //特例化的模板,实现对字符串大小的比较
bool comapre(const char* a,const char* b)
{
return strcmp(a,b);
}
中期总结:到此为止,compare
模板已经满足了基本的需要,类似的思想可供来开发最浅显的STL容器。
用模板来开发一个简易的Vector容器
直接上代码:
template<typename T>
class wVector
{
public:
wVector(int size = 10) //构造函数
{
_first = new T[size];
_last = _first;
_end = _first + size;
cout << "wVector consted!" << endl;
}
~wVector() //析构函数
{
delete[] _first;
_first = _last = _end = nullptr;
cout << "~wVector consted!" << endl;
}
wVector(const wVector<T>& other) //拷贝构造函数
{
int size = other._end - other._first;
_first = new T[size];
int len = other._last - other._first;
for (int i = 0; i < len; i++)
{
_first[i] = other._first[i];
}
_last = _first + len;
_end = _first + size;
}
wVector& operator=(const wVector<T>& other) //赋值重载函数
{
if (this == &other)
{
return *this;
}
delete[] _first;
int size = other._end - other._first;
_first = new T[size];
int len = other._last - other._first;
for (int i = 0; i < len; i++)
{
_first[i] = other._first[i];
}
_last = _first + len;
_end = _first + size;
return *this;
}
void wPush_Back(const T &val) //以下就是vector的基本操作了
{
if (full())
expand();
*_last++ = val;
}
void wPop_Back()
{
if (empty())
return;
--_last;
}
T back() const
{
return *(_last - 1);
}
bool full() const
{
return _last == _end;
}
bool empty() const
{
return _first == _last;
}
int size() const
{
return _last - _first;
}
private:
T* _first;
T* _last;
T* _end;
void expand()
{
int size = _end - _first;
T* ptmp = new T[size * 2];
for (int i = 0; i < size; i++)
{
ptmp[i] = _first[i];
}
delete[] _first;
_first = ptmp;
_last = _first + size;
_end = _end + 2*size;
}
};
代码不需要细读,功能实现上没问题,勉强能用。车虽然破,好歹能开。调用和普通的vector是一致的,不再赘述。这里我们有3个十分严重的情况要指出:
(1) 如果只是用基本数据类型,如int
,这样是没有问题的,但是如果需要用到自己实现的数据类型Test,我们看会有什么问题:
class Test
{
public:
Test()
{
cout << "Test constroed!" << endl;
}
~Test()
{
cout << "~Test constroed!" << endl;
}
};
int main()
{
wVector<Test> vec;
return 0;
}
我们在main函数中实例化了一个vec
,下面是事故重现:
可以看到,一个空容器vec
的构造和析构函数各执行了一次,但是离谱的是Test
的构造和析构函数执行了10次。
我们定位到问题代码的位置:
wVector(int size = 10) //构造函数
{
_first = new T[size]; //这里给T类型 new了10个对象
_last = _first;
_end = _first + size;
cout << "wVector consted!" << endl;
}
这句_first = new T[size]
,T
被转换为了Test
,new
运算符构造了10个Test
对象。为了方便精准定位根本原因,现在我们来画一下vec
目前的内存情况图:
vec
这个对象在构造时new出了10个Test对象。梳理一下:new
做了两件事,一是申请内存,二是构造对象。一个人干了两件事, 迟早要出事。分离职责事实上我们在这一步需要将申请内存和构造对象分开。这样就可以实现:在vec
的构造函数中只分配10个Test
对象所占的内存,而不去构造其对象。
(2)
再来看push_back()
的操作,
void wPush_Back(const T &val)
{
if (full())
expand();
*_last++ = val;
}
(3)
而pop_back()
的操作也是有问题的。
void wPop_Back()
{
if (empty())
return;
--_last;
}
为了解决上面的三个问题,我们需要自己操作内存分配。这里就牵扯到了空间配置器allocator。我们需要将内存分配/释放、对象构造/析构分离开,并将其内置到空间配置器这个类中。下面是allocator这个类模板的代码:
template<typename T>
struct Allocator //负责:内存开辟/内存释放 对象构造/对象析构
{
public:
T* allocate(size_t size) //负责内存开辟
{
return (T*)malloc(sizeof(T) * size);
}
void deallocate(void* p) //负责内存释放
{
free(p);
}
void construct(T* p, const T& val) //负责对象构造
{
new(p)T(val); //使用定位new,在指定的内存p上构造T类型对象val
}
void destroy(T* p) //负责对象析构
{
p->~T(); //调用p指向对象的析构函数
}
};
其中使用allocate
方法申请内存,使用construct
方法在allocate
申请的内存上构造对象。用destroy
方法调用指针p
指向对象的析构函数来析构p
指向的对象,再调用deallocate
释放p
指向的内存。
用Allocator改造Vector容器
前面已经分析目前我们自实现的vector容器的三个问题,以及空间配置器的好处。现在我们来重写一下容器。