文章目录
🧑💻第四章 模板🧑💻
前言:
模板(Template)指C++程序设计设计语言中采用类型作为参数的程序设计,支持通用程序设计,避免了程序员的重复劳动。
一、泛型编程
🎈
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢❓
在C++中,能够使用泛型编程可以解决这个问题。
⭐泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。
⭐模板是泛型编程的基础,模板分为函数模板和类模板
二、函数模板
1. 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2. 函数模板格式
template < typename T1, typename T2,......,typename Tn >
返回值类型 函数名(参数列表){ }
举例:👇
template<typename T> //typename是用来定义模板参数T的关键字,也可以使用class ,T是随便取的。
void Swap( T& left, T& right)
{ T temp = left; left = right; right = temp; }
3. 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
⭐本质上:本来应该由你来写的代码,然后你不想写重复的代码,你给一个模板,编译器通过模板照葫芦画瓢,帮你生成对应的代码。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于字符类型也是如此。但是C++的std库中提供了swap函数,我们不需要定义,直接调用即可。
4. 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化和显式实例化。
- 隐式实例化:这种让编译器根据实参推演模板参数的实际类型就是隐式实例化。
此时有三种处理方式:
Add((int)1.1, 2);//1. 用户自己来强制转化
2. 使用显式实例化
- 显式实例化:在函数名后的<>中指定模板参数的实际类型
Add<int>(1.1, 2); // 显式实例化
Add<double>(1.1, 2);// 显式实例化
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
⭐还有一种方法是函数模板给两个参数,让编译器自动推演。
template<class T1,class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
Add(1.1, 2);//先整型提升,再类型转换。
}
int Add(const int& left, const int& right)
{
return left + right;
}
int main()
{
Add(1.1, 2);//隐式类型转换
}
👆上述代码可以正常运行,主要是传参的时候会进行隐式类型转换。
但是,下面的代码就会运行错误👇,通过实参1.1将T推演为double类型,通过实参2将T推演为int类型,但模板参数列表中只有一个T,编译器根据实参推演模板参数的实际类型,编译器不知道要推演模板参数为什么类型而报错。
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
Add(1.1, 2);// 推演实例化报错 左边参数传过去推T是double,右边是int,矛盾
}
5. 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
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函数
}
❗模板调用,有现成匹配函数,绝对不会实例化模板。
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三、类模板
👇下面这种情况,再C语言中是没办法解决的,只能用C++中的类模板才能解决。
typedef int STDataType;
class Stack
{
private:
STDataType* _a;
int top;
int capacity;
};
int main()
{
Stack st1;//int
Stack st2;//char 这就没法解决,还要生成一个新的类
}
类模板解决方法👇:
template<class T >
class Stack
{
public:
Stack(size_t capacity = 4)
: _a(nullptr)
, _top(0)
, _capacity(0)
{}
private:
T* _a;
size_t _size;
size_t _capacity;
};
int main()
{
//类模板都是显示实例化 虽然用的一个类模板,但是Stack<int>,Stack<char>是两个类
Stack<int>st1;
Stack<char>st2;
}
类模板的定义格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{ // 类内成员定义 };
类模板的实例化:
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在< >中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Stack<int> s1;
Stack<double> s2;
🚨模板不支持分离编译,即声明放在.h,定义放到.cpp 。
四、非类型模板参数
⭐模板参数分为 类型形参 与 非类型形参
类型形参即:出现在模板参数列表中,跟在class或者typename之后的参数类型名称。
非类型形参,就是用一个整形常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
// 定义一个模板类型的静态数组
template<class T = int, size_t N = 10> //class T是类型形参,size_t N = 10是非类型形参
class A
{
public:
private:
T _a[N];
size_t _size;
};
int main()
{
A<int, 100>aa1; //100个元素
A<int, 1000>aa2;//1000个元素
A<> aa3; //类型形参可以用缺省值,非类型形参也可以用缺省值
return 0;
}
🚨
1.非类型模板参数只允许整形常量
2.非类型的模板参数必须在编译期就能确认结果
五、模板的特化
1.概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于在一些特殊场景下,函数模板和类模板不正确处理需要的逻辑,所以我们要针对一些情况进行特殊化处理,就要做模板特化。
例如:👇
template <class T>
bool Equal(const T& L ,const T& R)
{
return L == R;
}
int main()
{
cout << Equal(1, 2) << endl;//0
char p1[] = "hello";
char p2[] = "hello";
cout << Equal(p1, p2) << endl;//0
const char* p3 = "hello";
const char* p4 = "hello";
cout << Equal(p3, p4) << endl;//1
return 0;
}
可以看出Equal函数模板在大多数情况下都正常,但是针对字符串类型就出现错误,p1和p2位于栈区是不同空间的变量,p3和p4指向位于常量区的"hello"字符串,Equa内部并没有比较指针指向的对象内容,而比较的是指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
2.函数模板特化
- 必须要先有一个基础的函数模板。
- 关键字template后面接一对空的尖括号< >。
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
例如:对Equal函数进行特化👇
template <class T>
bool Equal(const T& L ,const T& R)
{
return L == R;
}
template <>
bool Equal<const char*>(const char* const& L, const char* const& R)
{
return strcmp(L, R) == 0;
}
int main()
{
const char* p3 = "hello";
const char* p4 = "hello";
cout << Equal(p3, p4) << endl;// 调用特化之后的版本,而不走模板生成了
return 0;
}
这样Equal(p3, p4) 就调用特化后的版本,而不走模板生成函数。
3.类模板特化
全特化
全特化即是将模板参数列表中所有的参数都确定化。偏特化
(1)部分特化
// 将第二个参数特化为int
template <class T>
class A<T, int>
{
private:
T _d1;
int _d2;
};
(2)参数更进一步的限制
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class A <T1*, T2*>
{
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class A <T1&, T2&>
{
private:
const T1& _d1;
const T2& _d2;
};
在调用类模板生成类对象时,会优先调用最匹配的模板。
int main()
{
A<double, int> d1; // 调用特化的int版本
A<int, double> d2; // 调用基础的模板
A<int*, int*> d3; // 调用特化的指针版本
A<int&, int&> d4(1, 2); // 调用特化的指针版本
return 0;
}
🚨类模板有全特化和偏特化之分,函数模板同样也可以分为全特化和偏特化
六、模板分离编译
什么是分离编译❓
一个程序由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
当模板分离时,编译时只有声明,没有函数定义,所以只能确定函数的名称,检查参数匹配,但是没有函数的地址,要求其生成的符号表中找对应的函数地址。
而函数模板在定义的地方仅仅是一个模具,没有实例化,所以没有函数地址,在符号表中找不到对应的地址,导致链接错误。
解决方法
1.将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。
2.模板定义的位置显式实例化。这种方法不实用,不推荐使用。
七、模板的优缺点
优点:
- 模板复用了代码,原本大量重复的工作,交给编译器去做,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性
缺点:
- 模板会导致代码膨胀问题,也会导致编译时间变长,为了减少代码膨胀,编译器会按需实例化。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
- 模板不支持分离编译。