【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;
}

【程序结果】

在这里插入图片描述

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

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

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

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

二、函数模板

2.1 函数模板概念

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

2.2 函数模板格式

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

// template是参数模板,定义的是类型
template<typename T1, typename T2, ......, typename Tn>
// 或者还能用class
template<classT1, classT2, ......, class Tn>

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

注意:typename是用来定义模板参数关键字,也可以使用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 函数模板的原理

  • 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实 模板就是将本来应该我们做的重复的事情交给了编译器
  • 模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换

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

在这里插入图片描述

通过观察反汇编我们发现:编译器并不是直接调用Swap函数,它是根据函数模板实例化生成具体函数。在编译阶段,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用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 显示实例化

在函数名后的<>中指定模板参数的实际类型,即不需要传递参数来推出T

// 输出的答案是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的类型。

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

template<typename T>
T* New(int n)
{
	return new T[n];
}

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;

// 存储int类型的栈
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;
};

// 存储double类型的栈
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.4 例子演示

以上代码可以使用类模板

#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++;
}

需要注意的是:

  • 普通类,类名和类型是一样
  • 类模板,类名和类型不一样(类型:类名<T>
  • 类模板分离要写多个template,因为template只会作用于一次它下面的类域
  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值