在我们学习C++时,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下。代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。所以就有了模板的出现。
什么时c++模板
某些函数(交换函数)的实现和所完成的功能基本相同,数据类型不同。而模板正是一种专门处理不同数据类型的机制。
模板是泛型程序设计的基础。
泛型编程
看一下下面这三个函数,如果学习过了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;
}
使用函数重载虽然可以实现,但是麻烦了。如果又有新的类型要交换的话,那我们又要ctrl CV一下,又要改参数类型。
这样代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否通过编译器,让编译器根据不同的类型来生成对应的代码呢?
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件 (即生成具体类型的代码),那将会节省许多头发。因此,C++的祖师爷呢就想到了【模版】这个东西,告诉编译器一个模子,然后其余的工作交给它来完成,根据不同的需求生成不同的代码。
模板分类
函数模板
类模板
函数模板
函数模板概念
函数模板格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
//……
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
一下就是一个模板的举例
template<typename T>
void Swap(T& left, T& right)
{
cout << "交换前" << endl;
cout << left << " " << right << endl;
T temp = left;
left = right;
right = temp;
cout << "交换前" << endl;
cout << left << " " << right << endl;
}
int main()
{
int a = 1;
int b = 0;
Swap(a, b);
double d = 1.23;
double f = 3.22;
Swap(d, f);
char ch = 'j';
char s = 'f';
Swap(ch, s);
return 0;
}
我们通过这个函数模版,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模版可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能。
函数模板原理
那么它们调用的是否是同一个函数呢?实际上并不是,虽然我们肉眼看它们是调用同一个函数(通过调试来看),但眼见不一定为实。通过调式转到反汇编可以看出,它们调用的不是同一个函数,它们所在的地址都不一样。
在编译器编译阶段 ,对于模板函数的使用, 编译器需要根据传入的实参类型来推演生成对应类型的函数 以供 调用。比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然 后产生一份专门处理 double 类型的代码 ,对于字符类型也是如此。

函数模板的实例化
隐式实例化:让编译器根据实参推演模板参数的实际类型
在发生函数模板的调用时,不显示给出模板参数而经过参数推演,称之为函数模板的隐式模板实参调用(隐式调用)。如:
template <typename T> void func(T t)
{
cout << t << endl;
}
int main()
{
func(5);//隐式模板实参调用
return 0;
}
但有一种情况会出现问题
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 1; int b = 2;
double d = 2.9;
cout << Add(a, d) << endl;//出错
return 0;
}
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。
注意:在模板中,编译器一般不会进行类型转换操作。
在考虑使用一个模板的情况下,此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 1; int b = 2;
double d = 2.9;
cout << Add<int>(a, d) << endl;//显示实例化
cout << Add<double>(a, d) << endl;//显示实例化
cout << Add(a, (int)d) << endl;//强转
return 0;
}
一下是试用两个模板的情况
template<typename T1, typename T2>
auto Add(const T1& left, const T2& right)
{
cout << "auto Add(const T1& left, const T2& right)" << endl;
return left + right;
}
int main()
{
int a = 1; int b = 2;
double d = 2.9;
cout << Add(a, d) << endl;
return 0;
}
显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 1; int b = 2;
double d = 2.9;
cout << Add<double>(a, d) << endl;
cout << Add<int>(a, d) << endl;
cout << Add<float>(a, d) << endl;
return 0;
}
有一种必须要显示实例化:
template<class T>
T* func(int a)
{
T* p = (T*)operator new(sizeof(T));
new(p)T(a);
return p;
}
int main()
{
int* ret1 = func<int>(1);
func<A>(1);//func(1);//假如不用返回值接收,那么就会报错,这时就必须要显示实例化了
A* ret2 = func<A>(1);
delete ret2;
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
为什么要有显示实例化?事实上,编译器只在要调用函数的时候才使用到函数,如果不使用显示实例化,每次调用函数时,模板都会消耗性能去推导使用的是哪个类型的函数,增加了程序运行时的负担;使用了显示实例化,则在编译时就已经处理了函数选择。
注意:试图在同一个文件中使用同一种类型的显示实例化和显示具体化声明,会出错。
template <typename T>
T add(T x,T y)
{
return x+y;
}
int main()
{
int i1=2,i2=3;
add<int>(i1,i2);
template int add<int>(i1,i2);//尽量用上面一行的写法代替本行
return 0;
}
模板参数的匹配原则
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
数
}
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
总结一下:
// 专门处理int的加法函数
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
// 通用加法函数
template<class T1, class T2>
auto Add(T1 left, T2 right)
{
cout << "auto Add(T1 left, T2 right)" << endl;
return left + right;
}
template<typename T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
//
// 1、都有的情况,优先匹配普通函数+参数类型匹配
// 2、没有普通函数,优先函数模版+参数类型匹配
// 3、只有一个,类型转换一下也能用,也可以匹配调用
int main()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1.1, 2.2); // 与函数模板类型完全匹配,需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
return 0;
}
类模板
类模板概念
类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似。
类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() {return _size;}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
类模板的实例化
typedef int DataType;
class Stack
{
public:
// 构造函数
Stack(int capacity = 3) //初始化列表
:_array(new DataType[capacity]) // 开辟一个DateType的动态数组,并进行初始化
, _capacity(capacity)
, _size(0)
{}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
delete[]_array;
_array = nullptr;
_size = _capacity = 0;
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s1;
return 0;
}
这是一个int 类型的栈,那么如果需要其它类型的栈呢?(double char) 有些人可能说这还不简单吗,
CV一份出来,然后把typedef int DataType中的int 改为其他类型就可以了。但是这样的话,代码复用性就低,很多都是重复的。
如果我们用模板就不用重复造轮子了。
template <typename T>
class Stack
{
public:
// 构造函数
Stack(int capacity = 3) //初始化列表
:_array(new T[capacity]) // 开辟一个DateType的动态数组,并进行初始化
, _capacity(capacity)
, _size(0)
{}
void Push(T data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
delete[]_array;
_array = nullptr;
_size = _capacity = 0;
}
private:
T* _array;
int _capacity;
int _size;
};
int main()
{
Stack<int> s1;
Stack<float>s2;
return 0;
}
虽然这会让编译器干更多的的活,但是我们的效率提高了呀。死道友不死贫道很好的形容。