1. 为什么要使用模板?
-
假如设计一个求两参数最大值的函数,在实践中我们可能需要定义四个函数:
-
这些函数几乎相同,唯一的区别就是形参类型不同。
-
需要事先知道有哪些类型会使用这些函数,对于未知类型这些函数不起作用。
-
其他可替代方法对比
替代方法 | 缺点 |
---|---|
重载方式 | 相同的代码复制了多次,有修改时候,多处相同代码都需要修改 |
借助父类,子类继承父类 | 1. 缺少类型检测的功能;2. 以后子类都需要继承父类,代码难以维护 |
预处理命令 | 1. 格式混乱; 2. 功能需求比较大时候,无法满足需求 |
2.模板的概念
- 所谓模板是一种使用无类型参数来产生一系列函数或类的机制。
- 若一个程序的功能是对某种特定的数据类型进行处理,则可以将所处理的数据类型说明为参数,以便在其他数据类型的情况下使用,这就是模板的由来。
- 模板是以一种完全通用的方法来设计函数或类而不必预先说明将被使用的每个对象的类型。
- 通过模板可以产生类或函数的集合,使它们操作不同的数据类型,从而避免需要为每一种数据类型产生一个单独的类或函数。
3 模板的分类
- 模板分为函数模板(模子)和类模板(模子),允许用户分别用它们构造(套印)出(模板)函数和(模板)类。
-
图显示了模板(函数模板和类模板),模板函数,模板类和对象之间的关系。
3.1 函数模板
定义格式
template <模板形参表>
<返回值类型> <函数名>(模板函数形参表)
{
//函数定义体
}
其中T为类型参数,它可用基本类型或用户自定义的类型。
模板形参表格式如下:
class <参数名>
或typename<参数名>
或<类型修饰> <参数名>
class 与typename 是没有却别的,class 出现的比较早,现在一般使用typename
template <typename T>
T const& max(T const & a, T const & b)
{
return a < b ? b : a;
}
使用模板
模板函数的使用,只需要传入对应的值即可
std::cout << "max(7, 41):"<< ::max(7, 42) <<std::endl;
// 显示指定类型
std::cout << "max(7, 42):"<< ::max<int>(7, 42) <<std::endl;
std::cout << "max(7.0, 42.0):"<< ::max(7.0,42.0) <<std::endl;
std::cout << "max<int>(7.0, 42.0):"<< ::max<int>(7.0,42.0) <<std::endl;
std::cout << "max<double>(7.0, 42.0):"<< ::max<double>(7.0,42.0) <<std::endl
std::string s1 = "aaaa";
std::string s2 = "aaaabb";
std::cout << "max(s1,s s2):"<< ::max(s1, s2) <<std::endl;
两个参数类型不相同时候会报错,例如
max(7.0, 42)
candidate template ignored: deduced conflicting types for
parameter 'T' ('double' vs. 'int')
inline T const& max(T const & a, T const & b)
注:通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生一个不同的实体。这种用具体类型代替模板参数的过程叫做实例化,它产生了一个模板的实例。
于是,模板被编译了两次,分别发生在:
(1)实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。
(2)在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用(该类型没有提供模板所需要使用到的操作)等。
重载函数模板
template <typename T>
inline T const& max(T const & a, T const & b)
{
std::cout << "all" << std::endl;
return a < b ? b : a;
}
inline int const& max(int const & a, int const & b)
{
std::cout << "int" << std::endl;
return a < b ? b : a;
}
int main()
{
int a = 7;
int b = 42;
int maxInt = ::max(a,b);
std::cout << maxInt << std::endl; // 调用非模板
std::cout << max<>(a,b) << std::endl; // 调用模板
std::cout << max<int>(a,b) << std::endl; // 调用模板
std::cout << max(1.0,2.0) << std::endl; // 调用非模板
std::cout << max("aaa", "bbb") << std::endl; // 调用模板
std::cout << max('42', 1) << std::endl; // 调用非模板
}
- 对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会优先调用非模板函数,而不会从该模板产生出一个实例。然而,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
- 可以显式地指定一个空的模板实参列表,这个语法好像是告诉编译器:只有模板才能匹配这个调用(即便非模板函数更符合匹配条件也不会被调用到),而且所有的模板参数都应该根据调用实参演绎出来。
- 因为模板是不允许自动类型转化的;但普通函数可以进行自动类型转换,所以当一个匹配既没有非模板函数,也没有函数模板可以匹配到的时候,会尝试通过自动类型转换调用到非模板函数(前提是可以转换为非模板函数的参数类型)。
3.2. 类模板
定义格式
template<typename T>
class 类名 {
//…
};
关键字typename(或class)后面的T是类型参数。在实例化类定义中,欲采用通用数据类型的数据成员,成员函数的参数或返回值,前面需要加上T。
类模板的内部可以想其他类一样,声明成员变量和成员函数;
在成员函数的实现中必须要限定这个模板类,成员函数实现逻辑就是函数模板;
栈的类模板
#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
private:
std::vector<T> elems;
public:
void push(T const & e); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const { // 是否空
return elems.empty();
}
};
成员函数实现
// 入栈
template <typename T>
void Stack<T>::push(T const& e)
{
elems.push_back(e);
}
// 出栈
template <typename T>
void Stack<T>::pop()
{
if (elems.empty()) {
throw std::out_of_range("out of range");
}
T e = elems.back();
elems.pop_back(); // 删除最后一个元素
return e;
}
// 返回栈顶原始
template <typename T>
T Stack<T>::top() const
{
if (elems.empty()) {
throw std::out_of_range("out of range");
}
return elems.back(); // 返回最后一个元素的拷贝
}
类模板不代表一个具体的、实际的类,而代表一类类。实际上,类模板的使用就是将类模板实例化成一个具体的类,它的格式为:
类名<实际的类型>对象名;
例如,使用上面的类模板,创建模板参数为char、int型的对象,语句如下:
Stack<char> charStack ;
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
std::cout << intStack.top() << std::endl;
- 只有那些被调用的成员函数,才会产生这些函数的实例化代码。对于类模板,成员函数只有在被使用的时候才会被实例化。显然,这样可以节省空间和时间;
- 另一个好处是,对于那些“未能提供所有成员函数中所有操作的”类型,你也可以使用该类型来实例化类模板,只要对那些“未能提供某些操作的”成员函数,模板内部不使用就可以。
- 如果类模板中含有静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。
切记,要作为模板参数类型,唯一的要求就是:该类型必须提供被调用的所有操作
类模板的特化
特化 和 重载类似,重载是函数名相同,形参不同,特化就是类名相同,类的具体类型不同;
为了特化一个类模板,你必须在起始处声明一个template<>,接下来声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<>
class Stack<std::string>
{
...
};
进行类模板的特化时,每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代。如:
void Stack<std::string>::push(std::string const& elem)
{
elems.push_back(elem);
}
局部特化
上面的特化,类模板被具体类型代替、所有的成员函数被重新定义,这个叫做全特化;有时候要求模板参数仍由用户控制,这个叫做偏特化或者局部特化;
类模板
template <typename T1, typename T2>
class Myclass //
{
};
如下几种特化
// 两个模板参数具有相同的类型
template <typename T>
class Myclass<T, T> //
{
};
// 第2个模板参数的类型是int
template <typename T>
class Myclass<T, int>
{
};
// 两个模板参数都是指针类型
template <typename T1, typename T2>
class Myclass<T1*, T2*> // 也可以使引用类型T&,常引用等
{
};
创建对象
Myclass <int, float> m1; // 使用 Myclass<T1, T2>
Myclass <float, float> m2; // 使用 Myclass<T, T>
Myclass <float, int> m3; // 使用 Myclass<T, int>
Myclass <int*, float*> m4; // 使用 Myclass<T1*, T2*>
如果创建对象时候,出现一个对象对应两个模板类,就会报错;
4. more
4.1
优点:
- 灵活性, 可重用性和可扩展性;
- 可以大大减少开发时间,模板可以把用同一个算法去适用于不同类型数据,在编译时确定具体的数据类型;
- 模版模拟多态要比C++类继承实现多态效率要高, 无虚函数, 无继承;
缺点:
- 易读性比较不好,调试比较困难;
- 模板的数据类型只能在编译时才能被确定;
- 所有用基于模板算法的实现必须包含在整个设计的.h头文件中, 当工程比较大的时候, 编译时间较长;
网友评论