zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

C++模板初阶

2023-04-18 14:24:38 时间

模板初阶

1.泛型编程

引入:如何实现一个通用的交换函数?

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}

void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}

......

虽然这里我们可以使用函数重载,但有几个不好的地方

①重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数

②代码的可维护性比较低,一个出错可能所有的重载均出错

因此,我们引入泛型编程:编写与类型无关的通用代码,是代码复用的一种手段

模板是泛型编程的基础

image-20230309163820061

2.函数模板

2.1 函数模板的概念

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

2.2 函数模板格式

关键字:template(翻译为模板)

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

注意:typename是用来定义模板参数关键字(代表类型),也可以使用class,但是切记不能使用struct代替class

因此上述问题可用函数模板解决:

template<typename T>
void Swap(T& a, T& b)
{
	T tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	double d1 = 2.0;
	double d2 = 1.0;
	Swap(d1, d2);

	char c1 = '2';
	char c2 = '3';
	Swap(c1, c2);

	int i1 = 10;
	int i2 = 20;
	Swap(i1, i2);
}

实现完成:

image-20230309170110672

2.3 函数模板原理

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

此过程可称之为模板的实例化:

其中根据调用的参数类型生成不同的函数的过程称之为推演

image-20230309184144913

image-20230309184516921

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

2.4 函数模板实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板实例化分为:隐式实例化和显式实例化

例如:

template<typename T>
T Add(const T& a, const T& b)
{
	return a + b;
}
int main()
{
	int a1 = 10;
	int a2 = 20;
	double d1 = 10.1;
	double d2 = 20.1;

	//自动推演实例化
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;

	cout << Add((double)a1, d1) << endl;
	cout << Add(a2, (int)d2) << endl;

	//显示实例化
	cout << Add<double>(a1, d1) << endl;
	cout << Add<int>(a2, d2) << endl;

}

image-20230309190117969

因此,可总结为:

隐式实例化:让编译器根据实参推演模板参数的实际类型

显式实例化:在函数名后的< >中指定模板参数的实际类型

若这里是多个参数,也同样可以实现不同类型的加运算:

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
 	return left + right;
}

2.5 模板参数的匹配原则

问题:函数模板和具体函数是否可以同时存在?

//专门处理int加法函数
int Add(int a,int b)
{
    return a+b;
}
//通用加法函数
template<class T>
T Add(T a,T b)
{
    return a+b;
}

1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数;

2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生一个(有现成的即用现成的!)

3.实例,如果模板可以产生一个具有更好匹配的函数,那么将选择模板;

4.模板函数不允许自动类型转换,但普通函数可以自动进行类型转换。

3.类模板

我们知道用typedef可以给类型重新命名,在数据结构中我们用的很多;设想这样的一种情况:我们需要构造一个存int类型的栈,然而我们同时需要一个存double类型的栈,这样我们更改typedef的对象好像就有些乏力了,因此我们引入了类模板

3.1 类模板定义格式

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

以栈为例:

template<typename T>
class Stack
{
public:
    //构造函数
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = )" <<capacity<<endl;

		_a = (T*)malloc(sizeof(T)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	
	void Push(const T& x)
	{
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	T* _a;
	int _top;
	int _capacity;
};

int main()
{
	// 类模板一般没有推演时机,函数模板实参传递形参,推演模板参数
	// 类模板一般显式实例化
	// 他们是同一个模板实例化出来的
	// 但是模板参数不同,他们就是不同的类型(即st1和st2是属于两个完全不同的类型)
	Stack<int> st1;
    
	Stack<double> st2;

	return 0;
}

3.2 []的重载

重载运算符[]可以让我们像使用数组一样使用某些自定义类型,实现对它的读取和写入操作:

#include<iostream>
#include<assert.h>
#define N  10


template<class T>
class array
{
public:
	//重载运算符[]可以让我们像使用数组一样使用某些自定义类型
	T& operator[](size_t i)
	{
		//断言检查是否越界
		assert(i < N);
		return _a[i];
	}
private:
	T _a[N];
};


int main()
{
	//int a2[10];
	//a2[20] = 0; //编译器检查不出越界访问
	//a2[10];  //对数组边界抽查

	array<int> a1;
	for (size_t i = 0; i < N; ++i)
	{
		// a1.operator[](i)= i;
		a1[i] = i;
	}

	for (size_t i = 0; i < N; ++i)
	{
		// a1.operator[](i)
		std::cout << a1[i] << " ";
	}
	std::cout << std::endl;

	return 0;
}

除了可以让自定义类型也可以像数组那样,同时我们亦可实现对数组下标越界的更严格的处理:

编译器对于数组越界的检查是不严格的,编译器只会对数组的边界进行抽查并且只有修改了数据才会报错,这种检查是很 不安全;而自定义类型中我们无疑可以可以对数组越界进行更加严格的检查,比如加上断言

image-20230310145736390