C++STL模板学习

C++STL模板学习

  • 函数模板
    • 模板的格式
    • 模板的原理
    • 模板的实例化
    • 模板匹配的原则
  • 类模板
    • 类模板的定义格式
    • 类模板的实例化
  • STL的六大组件
  • 非类型模板参数
  • 类模板的特化
    • 全特化
    • 偏特化
  • 模板分离编译

在前面的学习中了解到C++是支持函数重载的,我们可以给同一作用的函数起同一个函数名,只是彼此之间参数的类型不能相同。先简单看一个变量交换的代码

//整形变量的交换
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}
//浮点类型变量的交换
void Swap(double& a, double& b)
{
	double c = a;
	a = b;
	b = c;
}

这样看起来好像还可以,但是我们知道数据的类型还有多种,像char,short,long…,要是我们给每一种类型都写一个专门的交换函数,那么仅仅是为了交换一个数据,就写那么多代码,显得有点多余了。那么C++在这里有什么优化呢?

函数模板

既然那些可重载的函数彼此之间只是变量的类型不同,其他的操作都相同,那就可以给他定义一个模板,然后每次只需要类似“typedef”的操作,给函数的参数类型换一个名字,在模板中用这个名字来代替这个类型的操作,就显得方便很多。

模板的格式

template<typename T1,typename T2....,typename Tn>

//替换后的交换函数
void Swap(T& a, T& b)
{
	T c = a;
	a = b;
	b = c;
}

typename是用来定义模板的关键字,也可以用class来表示自定义的类,但是不能用struct来代替。

模板的原理

为什么要有模板呢?
C++STL模板学习_第1张图片
模板那么好用,那么关于模板的底层是怎么实现的呢?
可以把模板想象成一个模子,对于这个模子的模具,每个模具都有自己的名字,T1,T2.,…,Tn。在每次调用的时候,他只需要先获取模具对应的类型,然后依据对应的类型去推演真正操作的函数代码。
个人感觉这里的模具在获取对应类型的时候,就相当于我们的typedef,然后给这个类型起一个别名,然后使用这个函数。。。
C++STL模板学习_第2张图片

模板的实例化

当我们使用不同类型的参数使用模板的时候,这个过程就是模板的实例化。
模板参数的实例化可以分为隐式实例化和显示实例化。

  • 隐式实例化
    隐式实例化就相当于我们不告诉编译器对应变量的类型,让编译器在编译操作的时候自己去推演对应的函数,常用的有swap模板,min模板,max模板…

在使用隐式实例化的时候,如果变量的类型是相同的,编译器是可以通过,那要是不同的呢?
C++STL模板学习_第3张图片
我们发现编译器会报错,显示模板的第二个参数的类型不明确。既然这样,我们就来两个模具看看效果
C++STL模板学习_第4张图片
这样写的话好像显得代码比较乱,还会多出一个警告,浮点数类型转为整形类型会出现数据的丢失,为了解决这个警告,可以给他来一个强制类型转换。
C++STL模板学习_第5张图片
发现这里好像又出错了。。。报错是两个参数是(int , int),不能转为(int&,int&)。这是怎么回事,问题只能出在 (int)d,这里了。
为了排查错误,首先我先给模板的参数去掉引用类型,发现这个时候编译器的报错都取消了,那么问题一定是引用了。
如果在使用引用类型的时候,如果引用前后类型不匹配,就需要进行强制类型转换的问题。因为强制类型转换的过程会在内存中新开辟一段空间,来储存转换后的数据,这时这个数据是一个临时变量,他只能在转换的过程生效,所以说它具有常性,也就是只读性。而我们调用引用类型后,引用的类型具有可读可写的特性,就相当于把权限放大了,我们知道,权限是只能缩小,不能放大的。这就出现了权限的错误,解决这个问题可以给第二个参数加一个const 类型,表面他是只读的,但是这样就不能满足交换函数的本意了。
C++STL模板学习_第6张图片
也可以这样理解,引用相当于给一个变量起了一个别名,再来看看我们传的这个变量是一个double类型的,本名是d,但是我们强制类型转换后的那个数据又是什么名字呢?他没有名字。。。
解决这个问题,可以在调用swap函数之前,就给double类型的变量强制转换后的临时变量一个名字,让他变成可读可写的,然后就可以调用了。好恶心
C++STL模板学习_第7张图片
通过调用C++中的swap交换函数,我们发现其实C++也没有解决不同类型交换的问题,这个锅我不背
在这里插入图片描述

  • 显示实例化
    在做过一些C++使用栈和队列的题的时候,就会发现,有这样的一种定义方式stack< int > st,queue< int > qu…这就是显示实例化。

显示实例化就是在函数名后的<>中指定模板参数的实际数据类型,如果指定类型和参数类型不匹配,编译器会常识隐式类型转换,如果不能转换成功编译器就会报错。
C++STL模板学习_第8张图片

模板匹配的原则

嘿嘿,要是我们在定义了一个模板的情况下,还自己定义了一个重载的函数,那么编译器会执行哪一个呢?
C++STL模板学习_第9张图片
我们发现编译器并没有用模板推演出函数来执行,只是调用我们写的重载函数。

  1. 模板函数支持重载类型的,并且该函数模板还可以被实例化为这个非模板函数
    C++STL模板学习_第10张图片
    当我们对这个模板函数进行实例化后,发现他会调用模板函数来生成的函数。
  2. 对于有和模板重载的函数,如果其他条件都相同,编译器在调用的时候不会调用这个模板推演出来的函数,而是调用现成的函数。
    如果模板可以产生一个具有更好匹配的函数,就会选择调用模板推演的函数
  3. 模板不允许自动类型转换,但是普通函数可以进行自动类型转换。

类模板

类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};

类模板的实例化

类模板实力话相比普通函数来说,他是在类模板名字的后面加<>,然后把实例化的类型放在<>中,类模板的名字并不是真正的类,他实例化的结果才是真正的类。

// stack类名,stack才是类型
stack<int> s1;
stack<double> s2;

STL的六大组件

STL中有空间配置器,容器,配接器,仿函数,算法,以及迭代器几种组件
C++STL模板学习_第11张图片

非类型模板参数

模板参数分为类型形参和非类型形参

  • 类型形参:出现在模板参数的列表中,跟在class或者typename后面的参数类型名称
  • 非类型参数:用一个常量作为类模板的一个参数,在类中可以把这个参数当做一个常量来使用

没有模板,我们可以这样写一个模板类型的静态数组,但是数组的大小N,却只能是一个固定的数字,不能改变

const int N = 10;

template<class T>
class arr
{
public:
	arr()
		:_size(0)
	{}

private:
	T _arr[N];
	size_t _size;
};

int main()
{
	arr<int> a;//创造一个10个大小数组
}

利用模板的参数,我们可以这样来

template<class T, size_t N = 10>
class arr
{
public:
	arr()
		:_size(0)
	{}

private:
	T _arr[N];
	size_t _size;
};

int main()
{
	arr<int,10> a;//创造一个10个大小数组
	arr<int, 100> b;//一个100大小的数组
}

注意:

  1. 浮点数,类对象以及字符串是不可以作为非类型模板参数的
  2. 非类型模板参数必须在编译期间就可以确认结果

类模板的特化

特化就是针对模板在使用过程中的特殊情况,给这个案例一个特别处理。

对于这样的一个比较两个数大小的模板

template<class T1,class T2>
class cmp
{
public:
	cmp(T1 a,T2 b)
		:_a(a)
		, _b(b)
	{}
	//比较两个参数的大小
	void Compare()
	{
		if (_a < _b)
			cout << "_a < _b" << endl;
		else if (_a == _b)
			cout << "_a == _b" << endl;
		else if (_a > _b)
			cout << "_a > _b" << endl;
	}
private:
	T1 _a;
	T2 _b;
};

全特化

全特化就是在特化的时候,把模板的参数全部确定化

显然,对于前面的比较函数,当两个变量的类型是字符串char*的时候,直接比较大小就会出错的,我们就需要全特化一下

template<>
class cmp < char*, char* >
{
public:
	//涉及到指针,得要深拷贝
	cmp(char* a, char* b)
	{
		char* ta = new char[strlen(a) + 1];
		char* tb = new char[strlen(b) + 1];
		strcpy(ta, a);
		strcpy(tb, b);
		_a = ta;
		_b = tb;
	}
	~cmp()
	{
		delete[] _b;
		delete[] _a;
		_a = _b = nullptr;
	}
	//比较两个参数的大小
	void Compare()
	{
		int ans = strcmp(_a, _b);
		if (ans < 0)
			cout << "_a < _b" << endl;
		else if (ans == 0)
			cout << "_a == _b" << endl;
		else if (ans > 0)
			cout << "_a > _b" << endl;
	}
private:
	char* _a;
	char* _b;
};

类似于这样的,用strcmp来对字符串类型进行比较,然后返回结果

偏特化

任何针对模板参数进一步进行条件限制的特化版本。也就是值限制一部分

  • 部分特化

将模板参数类表中的一部分参数进行特化

template<class T1>
class cmp<T1,int>
{
public:
	cmp(T1 a, int b)
		:_a(a)
		, _b(b)
	{}
	//比较两个参数的大小
	void Compare()
	{
		if (_a < _b)
			cout << "_a < _b" << endl;
		else if (_a == _b)
			cout << "_a == _b" << endl;
		else if (_a > _b)
			cout << "_a > _b" << endl;
	}
private:
	T1 _a;
	int _b;
};
  • 参数进一步限制

针对模板参数进行更进一步的条件限制,从而设计出来的模板

指针类型的特化模板。这里没有写

template<class T1,class T2>
class cmp <T1*, T2*>
{
public:
	//涉及到指针,得要深拷贝
	cmp(T1* a, T2* b)
	{
		_a = a;
		_b = b;
	}
private:
	T1* _a;
	T1* _b;
};

引用类型的特化模板

template<class T1, class T2>
class cmp < T1&, T2& >
{
public:
	//涉及到指针,得要深拷贝
	cmp(T1& a, T2& b)
		:_a(a)
		, _b(b)
	{}
private:
	T1& _a;
	T1& _b;
};

模板分离编译

对于一个模板,我们把模板的声明放在.hpp文件中,把模板的定义放在.cpp文件中,最后在主函数中调用模板的功能函数,这样写可以吗?

//aa.hpp
#include 
#include 
using std::cin;
using std::cout;
using std::endl;
template<class T>
T Add(const T& a, const T& b);

//aa.cpp
template<class T>
T Add(const T& a, const T& b)
{
	return a + b;
}

//text.cpp
#include "aa.hpp"
int main()
{
	cout << Add(1, 2) << endl;
	return 0;
}

在编译的时候会出现这样的错误
C++STL模板学习_第12张图片
程序是在一个.obj文件中出错了,回想一下编译器编译程序的几个过程

  1. 预处理(进行宏替换)阶段
      预处理功能主要包括宏定义,文件包含,条件编译,去注释等。就是将以#开始的头文件展开,define和typedef 宏定义的名称进行替换,去掉注释,生成一个.i文件

  2. 编译(生成汇编)
      在这个阶段中,编译器首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言。生成一个.s文件

  3. 汇编(生成机器可识别代码)
      汇编阶段是把编译阶段生成的.s文件转成目标文件。生成.obj文件

  4. 链接(生成可执行文件或库文件)
      在成功编译之后,就进入了链接阶段,生成可以执行的.exe文件

C++STL模板学习_第13张图片

  • aa.obj文件中,编译器没有看到对Add模板函数实例化的过程,因此就不会生成具体的调用的函数。因为模板的作用只是在调用的时候才会根据类型推演具体的函数。
  • text.obj中,编译器发现了函数调用的过程,就使用call指令去找调用的函数,但是因为在aa.obj中并没有生成这个实例化后的函数,因此链接的时候会报错

具体流程类似于这样
C++STL模板学习_第14张图片
为了解决这一问题,我们需要把模板的声明和定义放在同一个XXX.hpp文件下。因为预处理的过程会进行头文件展开工作,然后在汇编中主函数调用哪个函数,就转换哪个函数。

你可能感兴趣的:(C/C++,c++,函数模板,类模板,STL六大组件)