探索C++模板
在我们学习C++的过程中,经常会遇到一类问题就是很多地方代码相同只是类型不同,我们就需要将同样的逻辑实现两遍,比如当我们实现一个交换函数swap的时候,交换int类型要用到,交换string类型也要用到,而由于类型不同我们就需要将这个函数实现两遍,C++中提供了模板来实现泛型编程,模板可以理解为一个模具,当我们需要int类型的代码时,系统去这个模具实例化一份int代码出来,需要string代码的时候实例化一份string代码出来。
模板是为了实现复用,可以实现类型参数化
模板分为类模板和函数模板,类模板就是整个类的内置类型未定,函数模板就是函数里边的内置类型未定,我们先来看函数模板
语法是 template <class/typename 形参名1,形参名2…>
形参的定义可以用calss也可以用typename
接下来我们自己实现一个交换模板函数
template<class T>
void my_swap(T& x, T& y)
{
T z;
z = x;
x = y;
y = z;
return;
}
void TestSeqList()
{
int a = 1;
int b = 2;
string c = "111";
string d = "222";
my_swap(a,b);
cout << a << b << endl;
my_swap(c,d);
cout << c << d << endl;
}
从结果来看int类型与string类型均完成了函数调用
可是我们要记住只能同类型的交换,在第一个模板T就是实例化出int类型,第二次调用模板实例化出的是string类型,T也就i是string类型,我们不能在一份实例化中T同时是int也是string。
显示实例化
模板的原理是当编译的时候编译器会从调用的函数那里推演出类型,然后利用模板实例化出代码,同时我们也可以在调用的时候就指定类型,这被叫做显示实例化
my_swap<int>(b,a);
模板类
模板类与模板函数的语法相同,声明了这个类是模板类之后,类里边的成员函数,成员变量等一切可以用于内置类型的地方。
template<class/typename 形参1,class 形参2>
class A
{}
下面是用模板类实现的一个链表
#pragma once
#include<iostream>
#include<string>
using namespace std;
template<class T>
struct ListNode
{
T _data;
ListNode* _next;
ListNode* _prev;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
List(const List& l)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
Node* ptr = l._head;
while (ptr->_next != l._head)
{
ptr = ptr->_next;
PushBack(ptr->_data);
}
}
List& operator=(const List l)
{
swap(_head,l._head);
return *this;
}
~List()
{
Clear();
_head->_next = NULL;
_head->_prev = NULL;
delete _head;
}
void Clear()
{
while (_head->_next != _head)
{
Erase(_head->_next);
}
return;
}
void PushBack(const T& x)
{
Insert(_head,x);
return;
}
void PopBack()
{
Erase(_head->_prev);
return;
}
void PushFront(const T& x)
{
Insert(_head->_next, x);
return;
}
void PopFront()
{
Erase(_head->_next);
}
T& Front()
{
return _head->_next->_data;
}
T& Back()
{
return _head->_prev->_data;
}
void Insert(Node* pos, const T& x)
{
Node* newnode = new Node;
newnode->_next = pos;
newnode->_prev = pos->_prev;
newnode->_data = x;
pos->_prev->_next = newnode;
pos->_prev = newnode;
}
void Erase(Node* pos)
{
pos->_prev->_next = pos->_next;
pos->_next->_prev = pos->_prev;
pos->_next = NULL;
pos->_prev = NULL;
delete pos;
}
void Print()
{
Node* node = _head->_next;
while (node != _head)
{
cout << node->_data;
node = node->_next;
cout << endl;
}
}
void Empty()
{
if (_head->_next == _head)
{
return 1;
}
return 0;
}
size_t Size()
{
size_t count = 0;
Node* ptr = _head;
while (ptr->_next !=_head)
{
count++;
ptr = ptr->_next;
}
return count;
}
private:
Node* _head;
};
非类型模板形参
1.非类型模板形参:模板的非类型形参也就是内置类型形参如:template<class T, int a> class B{};其中int a就是非类型的模板形参。
2、 非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。
3、 非类型模板的形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *,对象的引用或指针是正确的。
4、 调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。
5 、注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。
6、 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。
7 、sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。
8 、当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量比如:template <class T, int a> class A{};如果有int b,这时A<int, b> m;将出错,因为b不是常量,如果const int b,这时A<int, b> m;就是正确的,因为这时b是常量。
9. 非类型模板形参的形参和实参间所允许的转换:
1、允许从数组到指针,从函数到指针的转换。如:template <int *a> class A{}; int b[1]; A m;即数组到指针的转换
2、const修饰符的转换。如:template<const int *a> class A{}; int b; A<&b> m; 即从int *到const int *的转换。
3、提升转换。如:template class A{}; const short b=2; A m; 即从short到int 的提升转换
4、整值转换。如:template class A{}; A<3> m; 即从int 到unsigned int的转换
适配器
当我们实现完了链表的时候,试着用模板在实现一个队列,这个时候我们发现队列的代码基本与链表一致,这个时候我们可以用适配器来实现,具体的我们来看代码实现
template<class T,template<class> class Container = List>
class Queue
{
public:
void Push(const T& x)
{
return _con.PushBack(x);
}
void Pop()
{
return _con.PopBack();
}
T& Front()
{
return _con.Rront();
}
T& Back()
{
return _con.Back();
}
void Empty()
{
return _con.Empty();
}
size_t Size()
{
return _con.Size();
}
void Print()
{
return _con.Print();
}
protected:
Container<T> _con;
};
第一句是模板的模板参数,我们也可以用template<class T,class Container>代替,我们相当于在queue中实现了一个容器,任何相关的类型都可以适配这个容器,然后我们将List适配进去,当然我们可以给缺省参数,这样默认适配List,然后我们在Queue中的成员函数实现就可以套用List的成员函数。
模板的总结
优点:
1.增加了代码的复用性,节省资源,产生了STL来提高效率
2.让代码更加灵活
缺点:
1.代码不易维护,编译时间变长
2.代码更加繁复,编译错误的时候不好定位错误