1.前言
在我们学习c++的时候,常常会遇见要使用函数重载的情况。而当使用函数重载时,通常会使得我们编写很多重复的代码,这样就显得非常臃肿,并且效率非常的低下。
重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。
所以为了解决上述的问题,c++中专门提出了模版来解决这个问题。在前面已经讲解过一篇关于模版的初步知识,本篇博客主要讲解c++的模版进阶的知识。
2.什么是c++模版
程序设计中经常会用到一些程序实体:它们的实现和所完成的功能基本相同,不同的仅 仅是所涉及的数据类型不同。而模板正是一种专门处理不同数据类型的机制。
模板------是泛型程序设计的基础(泛型generic type——通用类型之意)。
函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数 的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类 型的代码。
2.1 范型编程的思想
范型编程:主要就是你刻画一个模子,然后其他人利用这个模子,生成出符合自己的,且是自己想要的东西。
2.2 c++模版的分类
c++模版主要分为函数模版和类模版
对于泛型编程和c++模版的分类的相关知识不了解的阅读下面文章:【c++基础(六)】模版初阶--泛型编程,类模板-CSDN博客
本章着重讲解的是模版的高阶操作::非类型模板参数、全特化、偏特化等,以及关于模板声明与定义不能分离(在两个不同的文件中)的问题。
3.非类型模版参数
3.1 什么是非类型模版参数
之前所使用的模板参数都是用来匹配不同的类型,如
int
、double
、Date
等,模板参数除了可以匹配类型外,还可以匹配常量(非类型),完成如数组、位图等结构的大小确定 。因此简单的说非类型模版参数就是指完成一些与类型无关的,与结构大小有关的模版参数。
3.2 问题的引入
假设我现在自定义了一个静态栈,栈的大小设置为100。然后我构建了一个int 的类型的栈st1,和一个double 类型的栈st2。那么我希望stl 的大小为100,st2 的大小为500,能不能实现呢?
#define N 100
// 静态栈
template<class T>
class Stack
{
private:
int _a[N];
int _top;
};
int main()
{
Stack<int> st1;
Stack<double> st2;
return 0;
}
显然目前是不能的,因为静态栈的大小已经确定了,无法被更改。那么有没有什么办法可以解决这个问题呢?--这个时候就要用到非类型的模版参数了。
3.3 非类型模版的使用
- 非类型模板形参 : 就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
- template <size_t N> // N 为模板参数中的 ------- 非类型模板形参
非类型模版参数的定义
在定义模板参数时,可以不再使用 class
或 typename
,而是直接使用具体的类型,如 size_t
,此时称为 非类型模板参数
PS:非类型模板参数必须为常量,即在编译阶段确定值
3.4 解决上述问题
利用 非类型模板参数 定义一个大小可以自由调整的 整型数组 类
// 静态栈
template<class T,size_t N=100>
class Stack
{
private:
int _a[N];
int _top;
};
int main()
{
Stack<int> st1;
Stack<double,200> st2;
return 0;
}
可以再加入一个模板参数:类型,此时就可以得到一个 泛型、大小可自定义 的数组
template<class T, size_t N>
class Stack
{
public:
T& operator[](size_t pos)
{
assert(pos >= 0 && pos < N);
return _arr[pos];
}
size_t size() const
{
return N;
}
private:
int _arr[N]; //创建大小为 N 的整型数组
};
int main()
{
Stack<int , 10> s1; // 大小为 10
Stack<double , 20> s2; // 大小为 20
Stack<char , 100> s3; // 大小为 100
// 输出它们的 类型
cout << typeid(s1).name() << endl;
cout << typeid(s2).name() << endl;
cout << typeid(s3).name() << endl;
}
这样就完美的解决了上面无法自己调整大小的问题。
非类型模版参数也支持缺省值
template<class T, size_t N = 100> //缺省大小为100
3.5 非类型模版的使用规则
非类型模板参数要求类型为 整型家族,其他类型是不行的。
举个例子
//浮点型,非标准
template<class T, double N>
class arr4 { /*……*/ };
这里使用的是非整型家族,这样会出现问题。
到此就可以总结出来非类型模版参数的使用规律了:
1.只能将整型家族作为非类型的模版参数
2.非类型的模版参数必须为常量,因为这个参数在编译阶段就要确定下来
整型家族:
char
、short
、bool
、int
、long
、long long
等
4.模版的特化
其实特化很好理解,就是把一个函数的模版,特化成针对某一类型或者某一具体需要的函数的使用。
举个例子来帮助理解:
一个函数比较指针类型
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
Date* p1 = new Date(2024, 7, 6);
Date* p2 = new Date(2024, 7, 8);
cout << Less(p1, p2) << endl;
return 0;
}
比较完了之后发现这样是会出现问题的。
- 也就是说,Less 绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。
分析上述问题出现的原因
Less只是比较了指针地址,而Less 内部并没有比较p1和p2指向的对象内容,而比较的是pl和p2指针地址,这就无法达到预期,而错误。
如何解决这个问题呢?
此时,就需要对 -------------- 模板进行特化处理。
即 : 在原模板类的基础上 , 针对特殊类型所进行特殊化的实现方式。
模板特化中分为 函数模板特化 与 类模板特化。
4.1 函数模版特化
函数模版特化也很好理解,就是把从 模版中复制一份出来,并给出实际的类型。
举个例子:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<int*>(int* left, int* right)
{
return *left < *right;
}
int main()
{
int* p1 = new int (6);
int* p2 = new int(8);
cout << Less(p1, p2) << endl;
return 0;
}
输出结果为:1
一般来说对less进行特化的话也可以这样写
bool Less(int* left, int* right)
{
return *left < *right;
}
4.2 类模板的特化
类模板的特化也很简单,也是从类模板中生成出一份具体类型的代码,用来专门处理这一个类型。类模板特化又分为全特化和偏特化
全特化
全特化指 将所有的模板参数特化为具体类型,将模板全特化后,调用时,会优先选择更为匹配的模板类。
简单一点来说:全特化 就是将模板参数列表中 所有的参数都确定话
举个例子:
假设有下面这样一个 Data 类,我希望 构造函数 打印出来的 d2 对象面 Tl 是 int , T2 是 double,有什么办法吗?
先使用类模板---然后再从类模板复制一份给出具体类型
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
return 0;
}
我们实例化 dl 和 d2 对象时,编译器会自动调用其默认构造函数,当我们打印的时候,可以看到实际上d2 对象里面还是 T1 和 T2 并不是我们想要的 int 和 double。
这个时候对T1,T2进行特化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Data<int, double>
{
public:
Data()
{
cout << "Data<int, double>" << endl;
}
private:
int _d1;
double _d2;
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
return 0;
}
这样打印出来的结果就对了。
总结:
对模板进行全特化处理后,实际调用时,会优先选择已经特化并且类型符合的模板。因为不用再重复生成而是直接选择最合适的。
偏特化
偏特化简单来说就是特化一部分,还有一部分使用模版。
也可以是多个部分全部给定具体的参数类型。
举个例子:
部分特化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 部分特化 -- 将第一个参数特化为double
template<class T2>
class Data<double, T2>
{
public:
Data()
{
cout << "Data<double, T2>" << endl;
}
private:
double _d1;
T2 _d2;
};
int main()
{
Data<int, int> _d1;
Data<double, double> _d2;
Data<double, char> _d3;
return 0;
}
参数全部给定,来使其只针对某一类型
// 基础模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 部分特化 -- 将第一个参数特化为double
template<class T2>
class Data<double, T2>
{
public:
Data()
{
cout << "Data<double, T2>" << endl;
}
private:
double _d1;
T2 _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;
};
// 主函数
int main()
{
Data<int, int> d1; // 调用基础的版本
Data<double, double> d2; // 调用部分特化的double版本
Data<int*, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(2, 4); // 调用特化的引用版本
return 0;
}
5.总结
模板是
STL
的基础支撑,假若没有模板、没有泛型编程思想,那么恐怕"STL"
会变得非常大 。可以说模版在c++中是重中之重,希望各位小伙伴好好理解一下。
模板的优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
模板的缺点
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
6.共勉
以下就是我对 【模板进阶】 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对 C++ 的理解,请持续关注我,谢谢大家!!!