文章目录
- 一.泛型编程
- 二.函数模板
- (1).函数模板的实例化
- (2).非类型模板参数
- 三.类模板
- (1).类模板的成员函数
- (2).非类型模板参数
- 四.模板的特化
- (1).函数模板特化
- (2).类模板特化
- 五.模板不支持分离编译
一.泛型编程
在引入泛型编程之前,我们先来看这样一个问题,怎么做到实现一个通用的交换函数呢?
在C语言阶段我们可能会像下面这样写,需要分别实现不同类型的交换函数,又由于C语言不允许出现同名函数,所以函数名也需要不一样,写起来很繁琐
void Swapi(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
void Swapd(double* e1, double* e2)
{
double tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
C++因为名称修饰规则支持了重载函数,虽然函数名可以一样,但还是要分别实现不同类型的交换函数,也很繁琐
void Swap(int& e1, int& e2)
{
int tmp = e1;
e1 = e2;
e2 = tmp;
}
void Swap(double& e1, double& e2)
{
double tmp = e1;
e1 = e2;
e2 = tmp;
}
我们也可以看出上述代码只是参数类型不一样,其他的实现过程和逻辑都是一样的,为了解决(每一个类型都要自己去实现一个函数出来)这样的繁琐的问题,我们引入了泛型编程
(1).所谓泛型编程,是以独立于任何特定类型的方式编写代码,使用泛型编程时,我们需要提供具体程序实例所操作的类或值
(2).模板是泛型编程的基础,模板是创建类或函数的蓝图或公式,我们给这些蓝图或公式足够的信息,让这些蓝图或公式真正的转变为具体的类或函数,这种转变发生在编译时
(3).模板支持将类型作为参数的程序设计方式,从而实现了对泛型程序设计的支持,也就是说C++模板机制允许将类型作为参数
二.函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
函数模板格式
template<typename T1, typename T2,......,typename Tn>
// 返回值类型 函数名(参数列表){}
上面的问题就得以解决了
template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用 int 类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码
(1).函数模板的实例化
(1).隐式实例化 : 让编译器自己根据实参的类型推导模板参数的类型
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 1, b = 2;
cout << Add(a,b) << endl;
}
(2).显示实例化 : 在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 1;
double b = 2.2;
cout<<Add<int>(a,b)<<endl;
cout<<Add<double>(a,b)<<endl;
}
模板参数的匹配原则
(1). 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
(2). 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板实例化出来的函数
// 非模板函数
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(1,2.0) // 调用编译器进行模板实例化的函数
}
(2).非类型模板参数
在模板参数列表里,还可以定义非类型参数,非类型参数代表的是一个值,既然非类型参数代表一个值,不是一个类型,肯定不能用typename/class关键字来修饰这个值,我们当然要用我们以往学习过的传统类型名来指定非类型参数了
当模板被实例化时,这种非类型模板参数的值,或者是用户提供的,或者是编译器自己推断的,但这些值必须都得是常量表达式,因为模板实例化发生在编译阶段
非类型模板参数的一些限制 :
(1). 浮点数、类对象以及字符串是不允许作为非类型模板参数的(只能为整形)。
(2). 非类型的模板参数必须在编译期就能确认结果。
#include<iostream>
using namespace std;
template<int a,int b>
int add1()
{
return a + b;
}
template<class T,int a,int b>
int add2(T c)
{
return c + a + b;
}
template<unsigned L1,unsigned L2>
int charscmp(const char(&p1)[L1],const char(&p2)[L2])
{
return strcmp(p1, p2);
}
int main()
{
cout << add1<1, 2>() << endl;
cout << add2<int, 1, 2>(5) << endl;
cout << add2<int, 1, 2>(1.6) << endl;
cout << charscmp("test2", "test") << endl;
}
三.类模板
类模板定义格式 :
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
编译器不能为类模板推断模板类型参数,因此,类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
#include<iostream>
#include<vector>
using namespace std;
namespace lyp
{
template<class T>
class vector
{
public:
vector()
:_a(nullptr)
,_size(0)
,_capacity(0)
{}
~vector()
{
delete[]_a;
_a = nullptr;
_size = _capacity = 0;
}
T& operator[](size_t pos)
{
return _a[pos];
}
int size()
{
return _size;
}
void push_back(const T& x)
{
// 需要动态增容
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
T* tmp = new T[newcapacity];
if (_a)
{
memcpy(tmp, _a, sizeof(T) * _size);
}
delete[]_a;
_a = tmp;
_capacity = newcapacity;
}
_a[_size++] = x;
}
private:
T* _a;
int _size;
int _capacity;
};
}
int main()
{
lyp::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
for (int i = 0; i < v1.size(); i++)
{
// v1.operator[](3)
cout << v1[i] << endl;
}
std::vector<double> v2;
v2.push_back(1.1);
v2.push_back(2.2);
v2.push_back(3.3);
v2.push_back(4.4);
for (int i = 0; i < v2.size(); i++)
{
cout << v2[i] << endl;
}
}
(1).类模板的成员函数
(1).类模板成员函数,可以写在类模板定义中,这种写在类模板定义中的成员函数会被隐式声明成inline函数
(2).类模板一旦被实例化之后,那么这个模板的每个实例都会有自己版本的成员函数,所以,类模板的成员函数是有模板参数的,因此,如果要把类模板成员函数的定义写到类模板定义的外面,须以关键字template开始,后接模板参数列表,同时,在类模板名后用<>将模板参数列表里的所有模板参数名列出来
(3).一个类模板可能有多个成员函数,当实例化模板以后,后续如果没有使用某个成员函数,则该成员函数不会实例化
template<typename T>
class myvector
{
public:
// 构造函数
myvector();
// 赋值运算符重载
myvector& operator=(const myvector& v);
// 会被隐式声明成内联函数
void func()
{
// .....
}
};
template<class T>
myvector<T>& myvector<T>:: operator=(const myvector& v)
{
// .....
}
(2).非类型模板参数
template<class T,int size = 10>
class myarray
{
public:
void func();
private:
T arr[size];
};
template<class T,int size>
void myarray<T,size>::func()
{
cout << size << endl;
return;
}
四.模板的特化
模板特化:就是在实例化模板时,对特定类型的实参进行特殊处理,即实例化一个特殊的实例版本
#include<iostream>
using namespace std;
template<class T>
bool IsEqual(const T& left, const T& right)
{
return left == right;
}
int main()
{
const char* p1 = "hello";
const char* p2 = "hello";
cout << IsEqual(p1, p2) << endl;;
return 0;
}
上面的模板在比较字符数组时并不会达到我们想要的效果,因为该模板比较的是字符数组的地址,比较的结果是不相等,但我们想要的结果是相等,所以需要模板特化
(1).函数模板特化
解决方案 :
#include<iostream>
using namespace std;
template<class T>
bool IsEqual(const T& left, const T& right)
{
return left == right;
}
template<>
bool IsEqual<const char*>(const char* const& left, const char* const& right)
{
return strcmp(left, right) == 0;
}
int main()
{
const char* p1 = "hello";
const char* p2 = "hello";
cout << IsEqual(p1, p2) << endl;
return 0;
}
(1). template <> : 空模板形参表
(2). compare<const char *> : 模板名字后指定特化时的模板形参即const char *类型,就是说在以实参类型 const char * 调用函数时,将产生该模板的特化版本,而不是泛型版本,也可以为其他指针类型定义特化版本如int *.
(3). (const char * const &v1, const char * const &v2)可以理解为: const char * const &v1, 去掉const修饰符,实际类型是:char* &v1,也就是v1是一个引用,一个指向char型指针的引用,即指针的引用,加上const修饰符,v1就是一个指向const char* 指针的 const引用,对v1的操作就是对指针本身的操作,操作方式与指针一致,比如*v1,是正确的;
注意这里的const char *, 由于形参是一个指向指针的const引用,所以调用特化版本时的实参指针类型(并非存储的数据的类型)可以为const也可以为非const,但是由于这里形参指针指向的数据类型为const char *(强调存储的数据是const),所以实参指针所指向的数据类型也必须为const,否则类型不匹配;
(2).类模板特化
(1) . 全特化
全特化即是将模板参数列表中所有的参数都确定化
#include<iostream>
using namespace std;
template<class T1,class T2>
class A
{
public:
A()
{
cout << "A<T1,T2>" << endl;
}
};
template<>
class A<int, int>
{
public:
A()
{
cout << "A<int,int>" << endl;
}
};
int main()
{
A<int, double> a;
A<int, int> aa; // 全特化
return 0;
}
(2). 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本
1). 部分特化
#include<iostream>
using namespace std;
template<class T1,class T2>
class A
{
public:
A()
{
cout << "A<T1,T2>" << endl;
}
};
template<class T>
class A<T, int>
{
public:
A()
{
cout << "A<T,int>" << endl;
}
};
int main()
{
A<int,double> a;
A<int, int> aa; // 偏特化
return 0;
}
2). 参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版
本。
#include<iostream>
using namespace std;
template<class T1,class T2>
class A
{
public:
A()
{
cout << "A<T1,T2>" << endl;
}
};
template<class T1,class T2>
class A<T1*, T2*>
{
public:
A()
{
cout << "A<T1*,T2*>" << endl;
}
};
template<class T1, class T2>
class A<T1&, T2&>
{
public:
A()
{
cout << "A<T1&,T2&>" << endl;
}
};
int main()
{
A<int,int> a;
A<int*, double*> aa; // A<T1*,T2*>
A<int&, double&> aaa; // A<T1&,T2&>
A<int*, double&> aaaa; // A<T1,T2>
return 0;
}
五.模板不支持分离编译
在编译阶段,每一个cpp文件都是相对独立的,并不知道另一个编译文件的存在,若存在外部调用,会在链接阶段进行重定位。
模板的实例化其实只能发生在本编译单元的调用。如果出现非本编译单元的模板调用,也就是分离式编译,只能等待链接时重定位,但是模板并没有实例化,所以会出现链接出错。
因此,建议将声明和定义放到一个文件 “xxx.hpp” 里面或者"xxx.h"
// a.h
template<class T>
T Add(const T& a, const T& b);
// a.cpp
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}