模板
说到模板,就先要提到函数重载,函数重载就是为了重新编写函数以便实现函数的其余功能,但是使用函数重载就必须针对所需想同行为的不同类型重新实现他。
以一个加法函数为例:
int Add(int left, int right)
{
return left + right;
}
float Add(float left, float right)
{
return left + right;
}
只要有新的类型出现,那么就必须去重新去重载,添加对应的函数;而且代码的类型都差不多,这样就增加了代码的复杂程度,复用率太低,而且最关键的是,函数重载不能解决函数返回值或返回类型不一样的情况,所以我们需要去采用别的方法。
不用函数重载,那么大家可能会想到去用继承的方式,将需要的主方法写在基类里面,然后在需要使用的时候继承基类,那么就可以使用了,但是这也有一个缺点就是你想要维护代码的时候,你必须要去查找基类,那么难度就会增加很多,所以这种方法也不是最好的。
当然,如果你去使用预处理的话这就更不适合了,你根本没有办法在编译期间查找出错误,如果错了你根本不知道错在哪里,所以,在这里我介绍一下模板。
模板抽象了具体的型别,实现了共性逻辑,为一类问题提供了统一的泛型接口,模板机制使得编程者在定义类和函数时能以类型作为参数,并且模板只依赖于实际使用时的传入参数,不关心能被用作参数的那些不同类型之间的任何联系。模板有函数模板和类模板,下面分别作介绍。
函数模板
函数模板代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参的类型产生具体的函数特定类型版本。
模板函数的关键字为template,格式为
template<typename T1, typename T2,......,class Tn>
返回值类型 (函数名)参数列表
{...}
其中template是定义模板关键字,而typename则是定义模板形参名字的关键字,和class一样可以定义任意的参数名,注意,定义参数名的关键字只有这两个,不可以用别的进行替代。
要注意到模板只是一个样本,他不是类或者函数,编译器会通过模板产生特定的类或者是函数的特定类型版本,产生模板特定类型的过程被称为模板的实例化。
以代码为例:
template <typename T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add(10, 20) << endl;
cout << Add(2.1, 3.6) << endl;
return 0;
}
在这里面,第一个Add(10,20)构成的是类似于int Add(int left,int right){return right+left;}这样的函数,而第二个则是double Add(double left,double right){return right+left;}这样的函数,这就是函数模板的实例化。但是,这还有其余的实例化方式:
template <typename T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add(10, 20) << endl;
cout << Add(2.1, 3.6) << endl;
cout << Add<int>(2.3, 1.3) << endl;
cout << Add(10, (int)2.3) << endl;
return 0;
}
在这其中,第三种调用的方式调用模板形成的也是int Add(int left,int right){return right+left;}这样的函数,而第四种则是将数据进行强转,传的得时同一种数据类型。
值得注意的是在进行实例化之前,模板是被编译了两次的:在实例化之前先检查模板代码本身是否有错;在实例化期间再检查模板的代码,看所有调用是否有效。
模板参数
函数模板有两种参数,分别是类型形参和非类型形参。
在使用模板参数的时候,有一些地方是需要注意的:
①注意名字屏蔽规则,不要使用已经被定义过的名字,另外参数名字只能在模板形参之后到模板定义的末尾之间;
②形参中的名字不能重复使用,一个名字在形参列表中只能出现一次;
③在形参名字前面必须加上class或者是typename关键字进行修饰;
④模板的定义只能放在开头,不能放在模板内部;
⑤模板的形参列表不能为空(但是要除去特化的情况);
当然,作为模板函数,它毕竟还是一个函数,当然可以进行函数重载,只是,你需要注意的是:函数所有的重载的声明都应该被放在该函数被调用的位置之前。
在这里,可能有人会想到,既然模板函数可以重载,那么同名的非模板函数和模板函数会构成重载么?那么我就需要问一下构成重载的条件是什么?
①在同一个作用域;②函数名相同;③参数列表不同。只要满足这三个条件,那么就可以构成重载,那么非模板函数和模板函数自然就可以构成重载了。不仅可以构成重载,这个函数模板还可以被实例化为这个非模板函数。既然已经构成重载了,那么在调用的时候会先调用哪一个函数呢?
还是用代码作为例子吧:
在输出函数这打一个断点,当函数进入下一步的时候,函数会调用哪一个函数?
可以看到会先去调用这个非模板函数,这就说明在条件相同的情况下,调用时会先去调用非模板函数而不是去调用模板生成一个实例。但是如果模板可以产生一个更好匹配的函数,那么会优先调用模板。
这是不是就是说模板很好用,没有什么缺点呢?
当然不是,在有些时候并不能写出很满足可能被实例化的类型的合适的模板,甚至某些情况下还会发生错误。下面就举个例子:
template <typename T>
int compare(T p1, T p2)
{
if (p1 < p2)
return -1;
if (p1 > p2)
return 1;
return 0;
}
int main()
{
char *pStr1 = "abcd";
char *pStr2 = "1234";
cout << compare(pStr1, pStr2) << endl;
return 0;
}
正常情况下,这里我们需要让它返回的应该是1,但是结果却是
这是为什么呢?
其实道理很简单,在调用函数的时候,直接将两个指针变量的地址传递给模板参数,在比较时,比较的只是指针的大小,并没有比较指针的内容。
所以,我在这里就引入了模板特化的概念。
模板特化
我们可以这样定义模板
template<>
int compare<const char*>(const char* const p1, const char* const p2)
{
return strcmp(p1, p2);
}
这里就是模板的特化,在某种意义上与函数重载有点类似。具体的就是如下:
template <typename T>
int compare(T p1, T p2)
{
if (p1 < p2)
return -1;
if (p1 > p2)
return 1;
return 0;
}
template<>
int compare<const char*>(const char* const p1, const char* const p2)
{
return strcmp(p1, p2);
}
int main()
{
const char* pStr1 = "abcd";
const char* pStr2 = "1234";
cout << compare(pStr1, pStr2) << endl;
return 0;
}
这样的结果就会如我们所想的为1么,我们来看一下
这样就得到了我们想要的结果了。
模板特化使用的方式很简单,在template后面接上<>,再接上模板名和<>,尖括号中指定这个特化定义的模板形参,然后就是()和括号中的形参列表,最后就是函数体。
至于使用模板特化,你就不得不注意到它的使用条件:
①模板特化必须与特定的模板相匹配;如果你没写模板或是名字不相同,又或是参数列表不一样,那么就会报错;
②模板特化中在模板名后面的<>中一定不能漏掉这个模板形参,如果漏了就只相当于定义了一个普通参数而已。
③这一点很重要,如果你模板和特化都已经写好且没有错误,那么在调用的时候,实参类型必须与特化后的模板的形参类型完全匹配,否则编译器将从模板中重新实例化一个实例。代码作证:
template <typename T>
int compare(T p1, T p2)
{
if (p1 < p2)
return -1;
if (p1 > p2)
return 1;
return 0;
}
template<>
int compare<const char*>(const char* const p1, const char* const p2)
{
return strcmp(p1, p2);
}
int main()
{
const char* pStr1 = "abcd";
const char* pStr2 = "1234";
char* pStr3 = "abcd";
char* pStr4 = "1234";
cout << compare(pStr1, pStr2) << endl;
cout << compare(pStr3, pStr4) << endl;
return 0;
}
在这里面,结果显示的是多少呢,大家可以想看看。
就是一个1,一个-1。
打上一个断点,当代码运行到这里的时候,他下一步会进入哪一个?
现在代码进入了模板特化的部分,在实参与形参匹配的情况下,特化实现了;但是当实参形参不匹配的时候,代码会进入哪里呢?
下一步会到哪里呢?
代码现在进入了模板的部分,说明在这里编译器通过模板生成了一个char*类型的函数。
这就充分的证实了我刚才说的实参形参需要完全匹配的情况。
模板类
上面介绍了模板函数,那么这里就得提到模板类了,模板类也还是模板,还是需要以关键字template开头,后接模板形参表。
基本格式就是
template<class 形参名1,形参名2,形参名3,......,形参名n>
class 类名{...};
就以一个普通顺序表为例,通常我们是这样定义一个顺序表:
#define DataType int
class SeqList
{
private:
DataType* _data;
int _size;
int _capacity;
};
但是,通过模板,我们就可以这样写:
template<typename T>
class SeqList
{
private:
T* _data;
int _size;
int _capacity;
};
在这里,我们使用动态顺序表作为例子进行分析template<typename T>
class SeqList
{
public:
SeqList();
~SeqList();
private:
int _size;
int _capacity;
T* _data;
};
template <typename T>
SeqList <T>::SeqList()
: _size(0)
, _capacity(10)
, _data(new T[_capacity])
{}
template <typename T>
SeqList <T>::~SeqList()
{
delete[] _data;
}
void test1()
{
SeqList<int> sl1;
SeqList<double> sl2;
}
int main()
{
test1();
return 0;
}
void test1()
{
SeqList<int> sl1;
SeqList<double> sl2;
}
test1这一部分去调用模板,int和double会分别由编译器进行模板推演,然后生成
class SeqList
{
private:
int _size;
int _capacity;
int* _data;
};
class SeqList
{
private:
int _size;
int _capacity;
double* _data;
};
编译器会重新编写SeqList类,最后创建名为SeqList<int>和SeqList<double>的类。
另外,在这里我需要提醒一下,模板类的类型不是SeqList,而是SeqList<T>,这一点很重要,务必记住,否则那些函数将不再是模板的类的成员函数。模板参数
先来看一下这部分的代码:
template <typename T>
class SeqList
{
private:
int _size;
int _capacity;
T* _data;
};
template <class T, class C = SeqList<T>>
class Stack
{
public:
void Push(const T& x);
void Pop();
const T& Top();
bool Empty();
private:
C _con;
};
void Test()
{
Stack<int> s1;
Stack<int, SeqList<int>> s2;
}
int main()
{
Test();
return 0;
}
里面的模板形参带上了缺省值,在调用的时候既可以用<int>,也可以用<SeqList<int>>去调用。这就表示这样使用是可以的。
模板的模板参数
这个和一般的区别在于哪里呢?看一下代码:
template <typename T>
class SeqList
{
private:
int _size;
int _capacity;
T* _data;
};
template <class T, template<class> class C = SeqList>
class Stack
{
public:
void Push(const T& x);
void Pop();
const T& Top();
bool Empty();
private:
C<T> _con;
};
void Test()
{
Stack<int> s1;
Stack<int, SeqList> s2;
}
int main()
{
Test();
return 0;
}
关键就在于里面标红的地方,template<class>就表示C是一个模板类类型的模板形参,它所定义的成员也是一个模板类类型。非类型的模板参数
之前就说过模板的参数被分为类型与非类型,那么这同样会有非类型的类模板参数,还是以代码为例:
template <typename T, size_t MAX_SIZE = 10>
class SeqList
{
public:
SeqList();
private:
T _array[MAX_SIZE];
int _size;
};
template <typename T, size_t MAX_SIZE>
SeqList <T, MAX_SIZE>::SeqList()
: _size(0)
{}
void Test()
{
SeqList<int> s1;
SeqList<int, 20> s2;
}
int main()
{
Test();
return 0;
}
带上缺省的模板参数,这样就是一种典型的非类型形参的模板参数。注意:浮点数和类对象是不允许作为非类型模板参数的。类模板的特化
既然模板函数有特化,那么模板类也会有特化,特化的基本定义是没有什么变化的,但是,和模板函数唯一不同的区别就在于,类模板的特化分为全特化和局部特化。全特化
全特化和模板函数的特化的区别不大,,还是一样的定义方式和使用方法。但是,需要提醒的是, 特化后定义成员函数不再需要模板形参。
局部特化
以代码为例:
template <typename T1, typename T2>
class Data
{
public:
Data();
private:
T1 _d1;
T2 _d2;
};
template <typename T1, typename T2>
Data<T1, T2>::Data()
{
cout << "Data<T1, T2>" << endl;
}
template <typename T1>
class Data <T1, int>
{
public:
Data();
private:
T1 _d1;
int _d2;
};
template <typename T1>
Data<T1, int>::Data()
{
cout << "Data<T1, int>" << endl;
}
这里就是在局部特化第二个参数;当然,局部特化当然不只是说特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data();
private:
T1 _d1;
T2 _d2;
T1* _d3;
T2* _d4;
};
template <typename T1, typename T2>
Data<T1 *, T2*>::Data()
{
cout << "Data<T1*, T2*>" << endl;
}
这里就是局部特化两个参数为指针,当然,局部特化引用也是可以的。
需要注意的是:模板的全特化和偏特化都是在已定义的模板基础之上,不能单独存在。
注:所有代码都是放在VS2013编译器下运行的。
更多推荐
模板的简单理解
发布评论