送给大家一句话:
尽管眼下十分艰难,可日后这段经历说不定就会开花结果。总有一天我们都会成为别人的回忆,所以尽力让它美好吧。 – 岩井俊二
\\\ ⱶ˝୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭兯 ////
\\\ ⱶ˝୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭兯 ////
\\\ ⱶ˝୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭兯 ////
1 前言
泛型编程是C++中十分关键的一环,泛型编程是C++编程中的一项强大功能,它通过模板提供了类型无关的代码,使得C++程序可以更加灵活和高效,极大的简便了我们编写代码的工作量。
泛型编程作为一种编程范式的主要优点包括:
- 代码复用:同一个算法或数据结构可以用于不同的数据类型,提高了代码的复用性。
- 性能:由于在编译时就已经知道具体的数据类型,因此编译器可以生成针对该类型的优化代码。
- 类型安全:泛型编程仍然可以进行类型检查,从而减少运行时错误。
泛型编程它允许开发者编写独立于数据类型的算法和函数。在C++中,泛型编程主要通过模板(Templates)来实现。模板允许编写代码时使用抽象的数据类型,这些数据类型在编译时会被具体的类型所替换。
我们来看一个简单的例子:假如我想要编写一个求和函数,那么传统的写法是:
//光是简单的这三种常见类型的自身我们都需要写许多代码!
int sum(int a, int b)
{
return a + b;
}
float sum(float a, float b)
{
return a + b;
}
double sum(double a, double b)
{
return a + b;
}
而通过使用模版就可以极大的简便我们的过程:
template<class A >
A sum(A a, A b)
{
return a + b;
}
使用一个函数就可以实现多种类型的求和,极大的提高了代码的复用率!下面我们就来学习模版!!!
C++中的模板分为两类:函数模板(Function Templates)和 类模板(Class Templates);
2 函数模板
什么是函数模版
函数模板(Function Templates):允许定义一个函数,它可以接受任何类型的参数。编译器会根据传递给函数的实际参数类型来实例化函数的特定版本。
上面的函数就是使用的函数模版。
template<class A >
A sum(A a, A b)
{
return a + b;
}
在这个例子中,sum
函数可以接受任何类型的参数(包括自定义类型),只要该类型支持比较操作。
如何使用函数模版
函数模版的格式是:
//需要几个模版就使用几个
template<typename T1, typename T2,......,typename Tn>
//写入对应函数即可
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
函数模板是C++中的一项强大特性,它本质上并非一个具体的函数实体,而更像是编译器生成具体类型函数的蓝图。当我们定义一个函数模板时,我们实际上是在描述一个能够处理多种数据类型的算法框架。编译器会根据这个框架,在程序中使用模板的具体实例时,自动生成对应的具体类型函数。只有使用了才会生成实例化函数哦!!!!
这样的设计理念,使得模板成为了一种将重复性的工作抽象化、自动化的工具,从而极大地提高了代码的复用性和开发效率。简而言之,函数模板让编译器承担了生成多样化函数实例的职责,让程序员能够专注于逻辑和结构,而不是繁琐的细节。
ps: 函数模版就像是让编译器干苦力,从而减去我们的工作量。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将A确定为double类型,然
后产生一份专门处理double类型的代码:
就这样编译器生成一个个函数,将模版实例化,这是一种隐式实例化。
我们在使用过程中可以通过显示实例化与隐式实例化来进行实例化:
- 显示实例化:在函数名后的<>中指定模板参数的实际类型
sum<int>(a,b)
,直接表明想要进行什么数据类型的函数即可。 - 隐式实例化:让编译器根据实参推演模板参数的实际类型,也就是正常使用函数,让编译器去处理类型(可能会发生类型转换,存在隐患)。
调用规则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
2 类模板
什么是类模版
类模板(Class Templates):允许定义一个类,其成员函数和方法可以操作任何类型的数据。与函数模板类似,编译器会根据使用时指定的类型来实例化类的特定版本。我们之前实现的vector等各种容器都使用到了类模版,通过类模版我们可以适配各种数据类型,省去重复造轮子的过程。
template <typename T>
class Stack {
public:
void push(T value);
T pop();
bool isEmpty();
private:
std::vector<T> elems;
};
在这个例子中,Stack
类可以被用来创建任何类型数据的堆栈。
如何使用类模版
与函数模版类似,我们在类声明的前面加上:
//需要几个模版就使用几个
template<typename T1, typename T2,......,typename Tn>
class ClassName
{
};
然后在类声明里面就可以直接使用我们的模版类型。
对于类模版的实例化是很关键的:
vector<int> num;
stack<string> st;
queue<char> q;
//在迭代器中更是好用
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
我们加入对应的模版参数即可!!!
C++标准模板库(Standard Template Library,STL)是泛型编程在C++中的一个典型应用,它提供了一系列模板化的数据结构和算法,如向量(vector
)、列表(list
)、队列(queue
)、栈(stack
)、排序算法等,这些都可以用于任何符合特定要求的类型。
4 特别注意
通过上述的介绍,就可以进行使用模版来进行代码的编写了。但是仍然有一些注意事项!!!
4.1 非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
就比如STL 中有一个这样的容器array
(很鸡肋,一般不使用,而且由于是静态数组,直接开在栈区,容易造成栈溢出),如果我们想要一个静态数组,就可以通过它来创建:
// 定义一个模板类型的静态数组
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;
};
int main()
{
array<int, 10> arr1;
array<int, 1000> arr2;
return 0;
}
通过传入的参数,编译器会生成两个不同的类,类的模版参数就是给定参数。
在C++20之前,只支持整型作为非类型模版参数(char , short , int , long long… )
4.2 模版缺省值
像函数参数一样,模版参数也支持使用缺省值!!!
使用缺省值就可以方便我们传入参数了:
//这里就是使用了缺省值
template<class T, size_t N = 10>
class array
{
};
另外再优先队列里也有很重要的使用:
// 默认底层容器是vector<T> , 默认用来比较的仿函数是 less<T>
template<class T , class Container = vector<T> , class compare = less<T> >
class priority_queue
{
};
4.3 编译细节
注意看下面的代码,我们在[ ] 重载中加入了一个size(1)
,明显不和语法规范,但是我们来看编译会出现什么现象:
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index) {
assert(index < N);
//明显错误
size(1);
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;
};
int main()
{
array<int, 10> arr1;
return 0;
}
来看:
我们的代码居然可以正常运行!!!这是怎么回事儿???
因为编译器在遇到模版时会进行下面操作:
- 根据模版的实例化形成模版半成品
- 实例化成具体的类/函数
- 进行语法编译
但是这里又增加了一个新的概念:按需实例化!!!没有实例化之前只会进行简单的框架检查。
也就是只有使用对应函数才会进行函数的实例化,才会进行语法编译,才会报错!!!
没有调用operator[ ],所以operator[ ] 有调用参数不匹配,就没有检查出来。
所以只有我们使用[ ] 重载函数时,才会进行检查!!!
4.4 模版特化
模版特化就是指把模版的参数进行确定,就进行了特殊化:
来看一段代码:
#include<iostream>
using namespace std;
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;
}
int main()
{
TestVector();
return 0;
}
我们运行看看:
得到的是这样的结果。因为如果类的模版参数与模版特化一致,那么就会进行特化的模版来进行实例化。
比较复杂一点点的用法是指针特化
class Data
{
public:
Data(int a, char b)
{
_d1 = a;
_d2 = b;
}
bool operator<(Data d)
{
return _d1 < d._d1;
}
private:
int _d1;
char _d2;
};
template<class T>
bool Less(T left, T right)
{
cout << "Less(T left, T right)" << endl;
return left < right;
}
template<>
bool Less<Data*>(Data* left, Data* right)
{
cout << "Less<Data*>(Data* left, Data* right)" << endl;
return *left < *right;
}
void TestVector()
{
Data* d1 = new Data(1,'c');
Data* d2 = new Data(2,'c');
Less(d1, d2);
}
int main()
{
TestVector();
return 0;
}
这样就是对指针进行特化
如果加上这个:
template<class T>
bool Less(T* left, T* right)
{
cout << " Less(T* left, T* right)" << endl;
return *left < *right;
}
那么就会优先执行这个指针模版。
总的来说,函数模版真不如直接使用函数重载!!!
特化分为:全特化与偏特化
- 全特化即是将模板参数列表中所有的参数都确定化
- 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
- 部分特化将模板参数类表中的一部分参数特化,如上面的例子。
- 参数更进一步的限制,偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。如上面的例子
类特化的是使用场景主要是在仿函数中进行使用,比如我们之前实现优先队列,在里面我们直接使用:
template<class T>
bool Less(T* left, T* right)
{
cout << " Less(T* left, T* right)" << endl;
return *left < *right;
}
可以适配更多的类型指针。
4.5 模版的分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模版不支持分离编译,如果声明与定义写到两个文件里,就会报错。
// 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;
}
这样就会发生报错!!!链接错误
链接错误:是在语法没问题情况下,链接的时候,一个函数声明去其他文件寻找函数定义,找不到就会发生链接错误。
那为什么寻找不到呢???明明我们写了函数定义。
因为 a.cpp下的函数定义没有实例化,调用函数时仅仅是声明知道了使用什么模版类型,而函数定义不知道使用什么模版参数,那自然无法实例化!!!文章来源:https://www.toymoban.com/news/detail-860535.html
解决方法很简单:文章来源地址https://www.toymoban.com/news/detail-860535.html
- 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h就很好的。推荐使用这种。因为.h文件预处理展开后,实例化模版时,既有声明又有定义,直接就实例化了,就有函数地址了。不需要链接时再去找。
- 模板定义的位置显式实例化。这种方法真不实用,真不推荐使用。这样模版还有什么意义!?
// a.cpp
//template<class T>
//T Add(const T& left, const T& right)
//{
// return left + right;
//}
template<>
int Add(const int& left, const int& right)
{
return left + right;
}
template<>
double Add(const double& left, const double& right)
{
return left + right;
}
Thanks♪(・ω・)ノ谢谢阅读!!!
下一篇文章见!!!
到了这里,关于【C++】从零开始认识泛型编程 — 模版的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!