[c++高阶]模版进阶

时间:2024-10-03 19:03:46

1.前言

在我们学习c++的时候,常常会遇见要使用函数重载的情况。而当使用函数重载时,通常会使得我们编写很多重复的代码,这样就显得非常臃肿,并且效率非常的低下

重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。

        所以为了解决上述的问题,c++中专门提出了模版来解决这个问题。在前面已经讲解过一篇关于模版的初步知识,本篇博客主要讲解c++的模版进阶的知识。

2.什么是c++模版

     程序设计中经常会用到一些程序实体:它们的实现和所完成的功能基本相同,不同的仅 仅是所涉及的数据类型不同。而模板是一种专门处理不同数据类型机制


       模板------是泛型程序设计的基础(泛型generic type——通用类型之意)。
 

        函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数 的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类 型的代码。

2.1  范型编程的思想

范型编程:主要就是你刻画一个模子,然后其他人利用这个模子,生成出符合自己的,且是自己想要的东西。

2.2 c++模版的分类

c++模版主要分为函数模版和类模版

对于泛型编程和c++模版的分类的相关知识不了解的阅读下面文章:【c++基础(六)】模版初阶--泛型编程,类模板-****博客

本章着重讲解的是模版的高阶操作::非类型模板参数、全特化、偏特化等,以及关于模板声明与定义不能分离(在两个不同的文件中)的问题。

3.非类型模版参数

3.1 什么是非类型模版参数

        之前所使用的模板参数都是用来匹配不同的类型,如 intdoubleDate 等,模板参数除了可以匹配类型外,还可以匹配常量(非类型),完成如数组、位图等结构的大小确定 。

因此简单的说非类型模版参数就是指完成一些与类型无关的,与结构大小有关的模版参数。

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.非类型的模版参数必须为常量,因为这个参数在编译阶段就要确定下来

整型家族:charshortboolintlonglong 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++ 的理解,请持续关注我,谢谢大家!!!