【C++】模版专题
个人主页:@我要成为c嘎嘎大王
希望这篇文章可以让你有所收获!
目录
一、模版引入
二、函数模版
2.1 函数模板概念
2.2 函数模板格式
2.3 函数模板的原理
2.4 函数模板的实例化
2.4.1 隐式实例化
2.4.2 显式实例化
2.5 模板参数的匹配原则
三、类模板
3.1 类模板的定义格式
3.2 类模板实例化
3.4 使用
3.4.1 多模板参数
3.4.2 成员函数定义
3.4.2 静态成员
四、类型模版参数与非类型模板参数
五、模板的特化
5.1 概念
5.2 函数模板特化
5.3 类模板特化
5.3.1 全特化
5.3.2 偏特化
六、模板分离编译
6.1 什么是分离编译
6.2 模板的分离编译
6.3 实现模板分离编译的方法
4. 模板总结
一、模版引入
如何实现一个通用的交换函数呢?
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;}......
使用函数重载虽然可以实现,但是有以下几个不好的地方:
1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增 加对应的函数
2. 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
C++模板是一种支持泛型编程的机制,允许编写与数据类型无关的代码。通过模板,可以定义函数或类,使其能够处理多种数据类型,而无需为每种类型重复编写代码。C++ 模板分为函数模板和类模板,通过泛型编程实现代码复用。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
template<class T>
void Swap(T& a, T& b)
{T tmp = a;a = b;b = tmp;
}
如果运行这个,不同类型的参数都可以实现交换,而不用函数重载就可以实现。
二、函数模版
2.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本。
2.2 函数模板格式
template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表){}
template<typename T>void Swap( T& left, T& right){T temp = left;left = right;right = temp;}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替 class)
2.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应 类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
2.4.1 隐式实例化
让编译器根据实参推演模板参数的实际类型
template<class T>T Add(const T& left, const T& right){return left + right;}int main(){int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);/*该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错Add(a1, d1);*/注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化Add(a, (int)d);return 0;}
2.4.2 显式实例化
在函数名后的<>中指定模板参数的实际类型
template<class T>T Add(const T& left, const T& right){return left + right;}
int main(void){int a = 10;double b = 20.0;// 显式实例化Add<int>(a, b);return 0;}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
2.5 模板参数的匹配原则
1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这 个非模板函数
// 专门处理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版本
}
2、 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理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. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三、类模板
3.1 类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
3.2 类模板实例化
template <typename T>
class MyContainer //注意,这里不是具体的类,是编译器根据被实例化的类型生成具体类的模具
{
private:T element;
public:void set(const T& value) { element = value; }T get() const { return element; }
};
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// MyContainer是类名,MyContainer<int>才是类型
MyContainer<int> intContainer;
3.4 使用
3.4.1 多模板参数
类模板支持多个类型或非类型参数,非类型参数可以是整型、指针或引用。
template <typename Key, typename Value, int Size>
class HashMap {
private:std::pair<Key, Value> data[Size];
public:void insert(const Key& k, const Value& v) { /* ... */ }
};
显示实例化:
HashMap<std::string, double, 100> map;
3.4.2 成员函数定义
成员函数可在类内或类外定义。类外定义需包含模板声明。
template <typename T>
class MyContainer {T element;
public:void set(const T& value);T get() const;
};template <typename T> // 类外定义需重复模板声明
void MyContainer<T>::set(const T& value) {element = value;
}template <typename T>
T MyContainer<T>::get() const {return element;
}
3.4.2 静态成员
类模板的静态成员需在类外单独定义,每个实例化类型拥有独立的静态成员副本。
template <typename T>
class Counter {
public:static int count;
};template <typename T>
int Counter<T>::count = 0; // 静态成员定义// 使用示例
Counter<int>::count++;
Counter<double>::count++; // int和double的count独立
四、类型模版参数与非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{public:T& operator[](size_t index){return _array[index];}const T& operator[](size_t index)const{return _array[index];}size_t size()const{return _size;}bool empty()const{return 0 == _size;}private:T _array[N];size_t _size;
};
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
五、模板的特化
5.1 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}int main()
{cout << Less(1, 2) << endl; // 可以比较,结果正确Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // 可以比较,结果正确Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比较,结果错误return 0;
}
可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
5.2 函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
int main()
{cout << Less(1, 2) << endl;Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl;Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了return 0;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
bool Less(Date* left, Date* right)
{return *left < *right;
}
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
5.3 类模板特化
5.3.1 全特化
全特化即是将模板参数列表中所有的参数都确定化。
template<class T1, class T2>
class Data
{public:Data() {cout<<"Data<T1, T2>" <<endl;}private:T1 _d1;T2 _d2;
};
template<>
class Data<int, char>
{public:Data() {cout<<"Data<int, char>" <<endl;}private:int _d1;char _d2;
};
void TestVector()
{Data<int, int> d1;Data<int, char> d2;
}
5.3.2 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
template<class T1, class T2>
class Data
{public:Data() {cout<<"Data<T1, T2>" <<endl;}private:T1 _d1;T2 _d2;
};
偏特化有以下两种表现方式: 部分特化——将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{public:Data() {cout<<"Data<T1, int>" <<endl;}private:T1 _d1;int _d2;
};
参数更进一步的限制——偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:T1 _d1;T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout<<"Data<T1&, T2&>" <<endl;}private:const T1 & _d1;const T2 & _d2;
};
void test2 ()
{Data<double , int> d1; // 调用特化的int版本 Data<int , double> d2; // 调用基础的模板Data<int *, int*> d3; // 调用特化的指针版本 Data<int&, int&> d4(1, 2); // 调用特化的引用版本
}
六、模板分离编译
6.1 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
6.2 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
C++模板的编译模型要求编译器在实例化模板时能够看到完整的定义。如果将模板定义放在源文件中,其他文件包含头文件时无法看到模板定义,导致链接错误。这是模板分离编译的主要技术障碍。
编译器首先会预处理,然后进行编译汇编最后是链接,有些时候模版的定义放在源文件里,由于没有给指定的类型,没有实例化,所以编译器也不知道到底识别成什么,只有一个声明没有定义,所以就没有生成具体的代码,最后链接的时候找不到,就会出错。
6.3 实现模板分离编译的方法
- 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
- 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
4. 模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
完
希望这篇小小文章可以为你解答疑惑!
若上述文章有什么错误,欢迎各位大佬及时指出,我们共同进步!