您现在的位置是:首页 >技术杂谈 >【C++初阶】初识模板网站首页技术杂谈

【C++初阶】初识模板

Weraphael 2024-09-16 12:01:04
简介【C++初阶】初识模板

在这里插入图片描述

?个人主页:@Weraphael
✍?作者简介:目前学习C++和算法
✈️专栏:C++航路
? 希望大家多多支持,咱一起进步!?
如果文章对你有帮助的话
欢迎 评论? 点赞?? 收藏 ? 加关注✨


一、 泛型编程

假设在一个项目中,要交换intchardouble等类型的数据,就要写出至少3个类型交换函数:

#include <iostream>
using namespace std;

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

void Swap(double& x, double& y)
{
	double tmp = x;
	x = y;
	y = tmp;
}

void Swap(char& x, char& y)
{
	char tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 1, b = 2;
	Swap(a, b);
	cout << "a = " << a << ' ' << "b = " << b << endl;

	double c = 1.1, d = 2.2;
	Swap(c, d);
	cout << "c = " << c << ' ' << "d = " << d << endl;


	char e = 'a', f = 'b';
	Swap(e, f);
	cout << "e = " << e << ' ' << "f = " << f << endl;

	return 0;
}

【程序结果】

在这里插入图片描述

以上代码就使用 函数重载,虽然可以实现交换逻辑,但是有一下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

答案当然是可以的!C++泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。

二、函数模板

2.1 函数模板概念

函数模板代表了一个函数家族,该 函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

2.2 函数模板格式

 typename -- 类型名(照抄)
 Tn -- 变量名(名字可以随便取)

template<typename T1, typename T2, ......, typename Tn>
// 或者还能用class
template<classT1, classT2, ......, class Tn>

返回值类型 函数名(参数列表) 
{
	// 代码逻辑
}

注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)。并且Tn也能做返回值。

2.3 例子演示

例如用函数模板把以上三个交换函数改成:

#include <iostream>
using namespace std;

template<typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}


int main()
{
	int a = 1, b = 2;
	Swap(a, b);
	cout << "a = " << a << ' ' << "b = " << b << endl;

	double c = 1.1, d = 2.2;
	Swap(c, d);
	cout << "c = " << c << ' ' << "d = " << d << endl;


	char e = 'a', f = 'b';
	Swap(e, f);
	cout << "e = " << e << ' ' << "f = " << f << endl;

	return 0;
}

【程序结果】

在这里插入图片描述

2.4 函数模板的原理

函数模板是一个蓝图,它本身并不是函数是编译器用使用方式产生特定具体类型函数的模具。所以其实 模板就是将本来应该我们做的重复的事情交给了编译器

我们可以通过反汇编来查看其底层原理:

在这里插入图片描述

通过观察反汇编我们发现:在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于其它类型也是如此

2.5 函数模板的实例化

2.5.1 概念

函数模板根据调用,编辑器会自己推导模板参数的类型,实例化出对应的函数,这称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化

2.5.1 隐式实例化

让编译器来推演模板参数的实际类型。

【例子】

#include <iostream>
using namespace std;

template<typename T>

T Add(const T& x, const T& y)
{
	return x + y;
}

int main()
{
	int a = 1, b = 2;
	double d1 = 1.1, d2 = 2.2;

	cout << Add(a, b) << endl;
	cout << Add(d1, d2) << endl;

	return 0;
}

【程序结果】

在这里插入图片描述

在以上代码中以Add(a, b)为例,ab在传参时,编辑器就推段出Tint类型。所以代码在编辑器的眼里是 以下这样的:

int Add(const int& x, const int& y)
{
		return x + y;
}

那现在假设是a + d1结果会是如何呢?

在这里插入图片描述

答案已经很明显了,当然不行!原因是:在编译期间,当编译器看到该函数实例化时,通过实参a推演T的类型为int,通过实参d1推演T的类型为double,然而模板参数列表只有一个T,因此编译器无法确定Tint还是double,故导致参数T不明确。

有两种方法可以解决以上问题

  • 第一种:用户自己强制转化
// 输出的答案是int类型
cout << Add(a, (int)d1) << endl;

// 输出的答案是double类型
cout << Add((double)a, d1) << endl;

【程序结果】

在这里插入图片描述

  • 第二种方法就要涉及显示实例化

2.5.2显示实例化

在函数名后的<>中指定模板参数的实际类型

// 输出的答案是int类型
cout << Add<int>(a, d1) << endl;

// 输出的答案是double类型
cout << Add<double>(a, d1) << endl;

【程序结果】

在这里插入图片描述

编译器会尝试进行 隐式类型转换,如果无法转换成功编译器将会报错。

但显示实例化在实现中并不常用,但以下场景是最经典的

假设要写一个函数,要求是返回一块动态开辟的空间

template<typename T>

T* New(int n)
{
	return new T[n];
}

int main()
{
	New(10); // 这是错误的

	return 0;
}

【程序结果】

**加粗样式**

New(10)是错误的。原因是:实参传给形参会推演类型,然而形参并没有T类型接收,就导致了编译器无法推导T的类型。

正确的方法是用显示实例化

int main()
{
	// 显示实例化
	int* p = New<int>(10); 

	delete p;

	return 0;
}

三、类模板

3.1 类模板的定义

// 定义格式

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

class 类模板名
{
	// 类内成员定义
};

3.2 为什么会有类模板(实际作用?)

假设要求每次实例化出的对象所存储的是不同类型的数据,那么有的人就会写n次不同类型的类,这无疑是增加了代码量(如下所示)。因此就有了类模板,编译器同样是根据实参推演出T的类型。

#include <iostream>
#include <stdlib.h>
using namespace std;

typedef int DataType;
class StackInt
{
public:
	StackInt(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
			return;

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	// 其他方法...

	~StackInt()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

typedef double DataType;
class StackDouble
{
public:
	StackDouble(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	// 其他方法...

	~StackDouble()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	StackInt s1; // int
	StackDouble s2; // double

	return 0;
}

3.3 例子演示

#include <iostream>
#include <stdlib.h>
using namespace std;

template<class T>

class Stack
{
public:
	Stack(int capacity = 3)
	{
		a = (T*)malloc(sizeof(T) * capacity);
		if (NULL == a)
		{
			return;
		}

		capacity = capacity;
		size = 0;
	}

	void Push(T data)
	{
		a[size] = data;
		size++;
	}
private:
	T* a;
	int capacity;
	int size;
};

3.4 类模板的实例化

类模板实例化与函数模板实例化不同, 类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,注意:类模板名字不是真正的类,而实例化的结果才是真正的类

int main()
{
	// 类模板的实例化
	Stack<int> s1;    // int
	Stack<double> s2; // double
	Stack<char> s3;   // char
}

3.5 类模板的声明和定义分离

#include <iostream>
#include <stdlib.h>
using namespace std;

template<class T>

class Stack
{
public:
	Stack(int capacity = 3);

	void Push(T data);

private:
	T* a;
	int capacity;
	int size;
};

template<class T>
Stack<T>::Stack(int capacity)
{
	a = (T*)malloc(sizeof(T) * capacity);
	if (NULL == a)
	{
		return;
	}

	capacity = capacity;
	size = 0;
}

template<class T>

void Stack<T>::Push(T data)
{
	a[size] = data;
	size++;
}

需要注意的是:

  • 普通类,类名和类型是一样
  • 类模板,类名和类型不一样

因此以上代码中,类名:Stack; 类型:Stack<T>

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。