目录
2.4.2 显式实例化:在模板函数名后加< >,并在尖括号里指定模板参数类型
1. 泛型编程
如何实现Swap呢?
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
void Swap(double & left, double& right)
{
double tmp = left;
left = right;
right = tmp;
}
//再有各种各样的类想交换,都要再写函数
//.....过于麻烦了吧
int main()
{
int a = 1, b = 2;
Swap(a,b);
double c = 1.1, d = 2.2;
Swap(c, d);
return 0;
}
虽然使用函数重载可以是现实,但是每次有新的类型都要写一个新的函数。且代码可维护性低,一个出错可能所有重载都出错。
所以引入模板,让编译器根据不同类型利用改模板生成代码。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
模板运行时不检查数据类型,也不保证类型安全。
2. 函数模板
2.1 函数模板格式
template<typename T1,typename T2,typename T3.....,typename Tn>//定义一个虚拟类型T,名字可以随便起。
//或者:template<class T>//用class和typename都行
#include<iostream>
using namespace std;
template<typename T>
void Swap(T & left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
int main()
{
char a = 'a', b = 'b';
Swap(a, b);
int c = 1, d = 2;
Swap(c,d);
double e = 1.1, f = 2.2;
Swap(e, f);
return 0;
}
2.3 函数模板的原理
上面不同类型的Swap调的都是模板函数吗?
不是。参数类型不同->开辟栈帧大小就不同。模板只是一个蓝图,并不是函数。实际上调用的是编译器根据模板生成的函数。
模板函数的实例化:用函数模板 实例化具体的我们想要的函数。让编译器根据实参推演模板参数的实际类型。
int类型的实参,用int替代T,生成一份处理int类型的代码。
实参是int类型,实例化一次。char类型实例化一次。double类型实例化一次。
实参类型相同的函数只会实例化一次该类型的函数,调用同一函数。
例:
int c = 1, d = 2;
Swap(c,d);
int x = 1, y = 2;
Swap(x, y);//都是int,不用实例化两次。调用的也是同一个函数。
template<typename T>
void Swap(T & left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 1;
double b = 1.1;
Swap(a, b);
return 0;
}
报错:模板参数T不明确,可能是double可能是int。
为什么不会发生类型转换,把int换成double?
提升或截断的隐式类型转换发生在赋值或传参时,但是这里报错在根据实参推演函数参数类型,还没实例化成功,也没有到传参调用那一步。
在下面说明解决方法。
2.4 函数模板的实例化
2.4.1 隐式实例化:让编译器根据实参推演参数类型
template<typename T>
T Add(const T& left, const T& right)//这里加const的原因是我们的例子会涉及隐式类型转换。(类型转换会产生中间变量,具有常性。引用不能权限放大,要加const)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 10.3;
cout << Add(a1, a2);
cout << Add(d1, d2);
//cout << Add(a1, d1)<<endl;//int+double 加不了哈哈
//解决方法一:把实参直接强转。方法二:显式实例化
cout << Add((double)a1, d1) << endl;
cout << Add(a1, (int)d1) << endl;
return 0;
}
2.4.2 显式实例化:在模板函数名后加< >,并在尖括号里指定模板参数类型
cout << Add<double>(a1, d1) << endl;
cout << Add<int>(a1, d1) << endl;//隐式类型转换
告诉编译器,不用推演了,按我们的想法实例化。
也可以定义多个模板参数解决上面问题。
template<typename T1,typename T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 10.3;
cout << Add(a1, d1)<<endl;//int+double,函数返回值类型是T1,所以输出整数类型 cout << Add(d2, a1) << endl;
return 0;
}
2.5模板参数的匹配原则
1.普通函数和同名的模板函数可以同时存在,且被实例化出来的函数可以和非模板函数一样。
template <typename T>
T Add(T left, T right)
{
cout << "T" << endl;
return left + right;
}
int Add(int left, int right)
{
return left + right;
}
int main()
{
int a = 1, b = 2;
Add(a, b);//优先调非模板参数,编译器也懒得自己推了
Add<int>(a, b);//这样子就调模板函数,被实例化出来的函数可以和非模板函数一样
return 0;
}
由此可知,由模板函数实例化出的函数的函数名修饰规则和普通函数不一样。
2.如果普通函数和模板函数同时存在,能不用函数模板就不用。调普通函数参数类型不匹配就会用函数模板实例化一个函数。
template <typename T1,typename T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
int Add(int left, int right)
{
return left + right;
}
int main()
{
int a = 1,c=2;
double b = 1.2;
Add(a, c);//与非模板函数 参数类型完全匹配,不实例化模板函数
Add(a, b);//调非模板函数还要隐式类型转换,所以编译器决定实例化模板函数,生成类型更匹配的函数。
return 0;
}
3.模板函数不允许自动类型转换,普通函数可以自动类型转换
3.类模板
3.1定义和实例化类模板
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
// 类模板函数在类外定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
// Vector是类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
类模板中的成员函数全是模板函数。
3.2类模板定义和声明分离定义
拿栈距举例:我们用 typedef 来解决可维护性,想要int的栈就 typedef int STDataType;
想要double的栈就typedef double STDataType;
但当我们想要两个栈,一个int,一个double就很麻烦了。可以写两个类(class StackInt和class StackDouble),不用typedef。或者使用类模板。
typedef增强的是可维护性,让我们尽量少的改动并复用代码,不是泛型编程。泛型编程要求编写和类型无关的代码。
栈:
template<typename T>
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" <<capacity<<endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(const T& x)//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
//类模板一般没有推演的时机,函数模板实参传递形参,推演模板参数
//类模板显示实例化
Stack <double> st1;
st1.Push(1.1);
Stack <int>st2;
st2.Push(1);
//st1和st2是同一个类模板实例化出来的,但是模板参数不同,就是不同的类型
return 0;
}
array:
#define N 10
template<class T>
class Array//不能用小写array,会和库里面的冲突。或者执意不想改名字,可以用命名空间。
{
public:
//毕竟是函数调用,会建立栈帧影响效率。但用上inline以后基本不会影响效率了
inline T& operator[](size_t i)//引用返回,可以改变实体
{
assert(i < N);//加了assert强制检查,越界就一定会报错
return _a[i];
}
private:
T _a[N];
};
int main()
{
//int a2[10];
//a2[10] = 0;//越界了,能检查出来
//a2[20] = 0;//没检查出来,因为编译器对静态数组越界访问的检查是抽查的
//
//a2[10];//越界读检查不出来,写还是抽查,只读检查不出来越界
Array<int> a1;
for (size_t i = 0; i < N; ++i)
{
a1[i] = i;
}
for (size_t i = 0; i < N; ++i)
{
cout << a1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < N; ++i)
{
a1[i]++;
}
for (size_t i = 0; i < N; ++i)
{
cout << a1[i] << " ";
}
cout << endl;
a1[20];//越界读也可以被检查到
a1[10];
return 0;
}
模板类的成员函数不支持定义和声明分离。
Stack.h(写函数声明) Stack.cpp(定义Stack类) Test.cpp(创建Stack对象并调用函数)
.h 会在两个.cpp展开,然后就没有.h了, 预处理变更成.i
|
预处理
Stack.i Test.i
|
编译(生成汇编指令)
Stack.s Test.s
语法问题会在编译阶段报错
Stack.s里面都是函数定义和声明。Test.
s里面有函数声明,编译就可以通过,生成call指令。但是找不到函数地址。
|
汇编(翻译成机器能看懂的二进制代码)
Stack.o Test.o
| .o文件合并
链接(生成可执行程序)
xxx.exe / a.out
链接的时候找函数地址。在Stack.o的符号表里找函数地址。
为什么模板定义和声明分离会报链接错误?
Stack里定义函数,模板要实例化出来才能有函数地址,而Stack里没实例化,就没有函数地址,没法进符号表里。Test里在定义Stack对象的时候显示实例化,但是只有声明,没有定义,也没有实例化出函数。链接的时候Test文件内部没有函数定义,去Stack.o的符号表里找函数也找不到。
解决方法一(不常用):定义和声明分离,在声明里显式实例化。
缺点是换个类型的栈还要重新实例化,还是不算泛型编程。
Stack.cpp文件
#include"Stack.h"
template<typename T>//每次都要再声明一下模板参数
Stack<T>::Stack(int capacity)//Stack <int>st2; 模板类的类型是Stack<T>
{
cout << "Stack(int capacity = )" << capacity << endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
template<typename T>
Stack<T>::~Stack( )
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
template<typename T>
void Stack<T>::Push(const T& x)//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
{
// ....
// 扩容
_a[_top++] = x;
}
template
class Stack<int>;//显式实例化
Stack.h文件
#include<iostream>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int capacity = 4);
~Stack();
void Push(const T& x);//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
private:
T* _a;
int _top;
int _capacity;
};
解决方法二:声明和定义分离,并放在同一个.h文件。
不需要链接了,当.h展开的时候,既有声明又有定义,不用再去链接找函数地址了,直接就是知道函数地址,展开后和main函数在一起,显示实例化的时候把定义也实例化了。
那为啥模板类的成员函数不直接定义在类里面呢?
为了可读性。方便能看到都有什么方法。
using namespace std;
#include<iostream>
template<typename T>
class Stack
{
public:
Stack(int capacity = 4);
~Stack();
void Push(const T& x);//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
private:
T* _a;
int _top;
int _capacity;
};
template<typename T>//每次都要再声明一下模板参数
Stack<T>::Stack(int capacity)//Stack <int>st2; 模板类的类型是Stack<T>
{
cout << "Stack(int capacity = )" << capacity << endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
template<typename T>
Stack<T>::~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
template<typename T>
void Stack<T>::Push(const T& x)//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
{
// ....
// 扩容
_a[_top++] = x;
}