【C++】函数模板的深入浅出

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41035588/article/details/84309211

模板是C++的一个重要特性。利用模板机制可以显著减少冗余信息,能大幅度地减少程序代码,进一步提高面向对象程序的可重用性和可维护性。

一、什么是模板

模板实现代码重用机制的一种工具,它可以实现类型参数化,即 把类型定义为参数,从而实现了代码的复用,使得一段程序可以处理多种不同类型的对象,大幅度提高了程序设计的效率。

二、模板的分类

模板分为函数模板和类模板,它们分别允许用户构造模板函数和模板类,下面用一张图来简单阐述一下模板、模板函数、模板类以及对象之间的关系
在这里插入图片描述
既然了解了模板的功能以及分类,下面我们将具体的对模板开拓。

三、函数模板

函数模板是 通用的函数描述,也就是说,他们使用泛型来定义函数。其中的泛型可用具体的类型(如int 或 double)替换,通过将类型作为参数递给模板,可使编译器生成该类型的函数。

在没有学习函数模板之前,如果让你写一个通用的加法函数,你会怎么写呢?可能第一个想到的就是函数重载。

int Add(const int& left,const int& right)
{
	return left + right;
}
float Add(const float& left,const float& right)
{
	return left + right;
}

这样写的程序的确没错,但是其缺点如下:
只要有新的类型出现,就要重新添加对应的函数
除类型外,所有函数的函数体都一样,代码的复用率太差
如果函数只是返回值类型不同,函数重载就不能解决
一个方法有差错,所以的方法都会有问题,都需要更改,维护性差。

这时你肯定想到了宏定义:比如这样:

#define Add ((left)+(right))

学过编译原理都知道,宏定义将在预处理阶段进行宏替换,并且宏定义不是函数,不会进行参数类型的检查,安全性不高。

最后你又想到了使用公共基类,将通用的代码放在公共的基础类里面。然而这也是一个不安全的实现,比如:
当你借助公共基类来编写一个通用代码时,将失去类型检查的安全性。
对于以后实现的类,都必须继承自某个特性的基类,代码维护更加困难

当上述三种方法都不能满足时,这时迎来了函数模板的到来,它代表了一个函数家族,函数之间与类型无关,在使用时被参数化,根据实参的类型来产生特性的类型版本。

四、函数模板的格式

template<typename Type1, typename Type2, ..., typename Type>
返回值类型  函数名(参数列表)
{
	...
}

其中typename是定义模板类型的关键字,可以使用class来替换。但是不能使用struct来代替typename。

模板函数也可以定义为inline函数,比如下面的程序:

templat<typename T>
inline T Add(const T& left, const T& right)
{
	return left + right;
}

但是需要注意的是inline关键字必须放在模板形参表之后,返回值之前,不能放在template之前。

模板是一个蓝图,它本身不是类或者函数,编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化,下面请看这个程序:

#include <iostream>
using namespace std;
template<typename T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	cout << Add(1, 2) << endl;
	cout << Add(1.3, 2.3) << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
模板被编译了两次。注意:
实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号。
在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调用。

那么函数模板是否会像普通函数那样隐式转换呢?看下面这个程序:

#include <iostream>
using namespace std;
template<typename T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	cout << Add(1, 2.3) << endl;//报错,说明不会隐式转换
	cout << Add<double>(1, 2.3) << endl;//必须显示强制转换
	cout << Add<int>(1, 2.3) << endl;
	cout << Add(1, (int)2.3)<< endl;
	system("pause");
	return 0;
}

在这里插入图片描述

五、模板参数

函数模板有两种类型参数:模板参数和调用参数。

模板参数名字只能在模板形参之后到模板声明或定义的末尾之间使用,遵循名字屏蔽规则。

#include <iostream>
using namespace std;
typedef int T;
template<typename T>
void FunTest(T t)
{
	cout << "t Type:" << typeid(t).name() << endl;
}
T gloab;
int main()
{
	FunTest(20);
	cout << "gloab Type:" << typeid(gloab).name() << endl;
	system("pause");
	return 0;
}

在这里插入图片描述

模板形参的名字在同一模板形参列表中只能使用一次。

template<class T>
void FunTest(T t1,T t2)
{

}
//重定义模板参数 " T "

所有模板形参前面必须加上class或者typename关键字。

template<typename T, U>
void Fun(T t,U u)
{

}
//未标识的表示符"U"

六、类型形参转换(类型不确定)

一般不会转换实参以匹配已有的实例化,相反会产生新的实例。

编译器只会执行两种转换:

const 转换,接收const引用或者const指针的函数可以分别用非const对象的引用或指针来调用。

#include <iostream>
using namespace std;
template<class T>
void FunTest(const T t)
{
	int _t = 2;
	t = &_t;
	cout << "t = " << *t <<endl;
}
int main()
{
	int a = 1;
	int *pa = &a;
	FunTest(pa);
	system("pause");
	return 0;
}

在这里插入图片描述
此时我们将FunTest函数中的const去掉会发现:
在这里插入图片描述
数组或函数到指针的转换,如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换,数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。

#include <iostream>
using namespace std;
template<class T, class U>
void FunTest(T t, U u)
{
	cout << typeid(t).name() << endl;
	cout << typeid(u).name() << endl;
}
void FunArray()
{
	int a = 10;
}
int main()
{
	int array[10] = { 0 };
	FunTest(array, FunArray);
	system("pause");
	return 0;
}

在这里插入图片描述

七、非类型形参转换(类型确定)

非模板类型形参是模板内部定义的常量,在需要常量表达式的时候,可以使用非模板类型参数。

例如数组长度:

#include <iostream>
using namespace std;
template<class T,int N>
void FunTest(T (&_array)[N])
{
	for (int idx = 0; idx < N; ++idx)
	{
		_array[idx] = 0;
	}
}
int main()
{
	int a[5];
	char b[5];
	FunTest(a);
	FunTest(b);
	system("pause");
	return 0;
}

在这里插入图片描述

八、模板参数说明

1、模板形参表使用<>括起来。
2、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同。
3、定义模板函数时模板形参表不能为空。
4、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后。
5、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型 使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换。
6、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。 但关键字typename是作为C++标准加入到C++中的,旧的编译器可能不支持。

九、模板函数的重载

下面看一个比较大小的程序:

#include <iostream>
using namespace std;
int Max(const int& left, const int& right)
{
	return left > right ? left : right;
}
template<class T>
T Max(const T& left, const T& right)
{
	return left > right ? left : right;
}
template<class T>
T Max(const T&a, const T&b, const T&c)
{
	return Max(Max(a, b), c);
}
int main()
{
	cout << Max(10, 20) << endl;
	cout << Max<double>(10, 20.2) << endl;
	cout << Max(10, 20, 30) << endl;
	cout << Max<double>(10, 20.1, 30.2) << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
需要注意的是函数的所有重载版本的声明都应该位于该函数被调用位置之前。

1)模板函数重载的几点说明

1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
2、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
3、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参演绎出来。
4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换 函数模板特化

有时候并不总是能够写出对所有可能被实例化的类型都合适的模板,在某些情况下,通用模板定 义对于某个类型可能是完全错误的,或者不能编译,或者做一些错误的事情。比如下面比较字符串的程序:

#include <iostream>
using namespace std;
template<typename T>
int Compare(T str1, T str2)
{
	if (str1 > str2)
		return 1;
	if (str1 < str2)
		return -1;
	return 0;
}
int main()
{
	char *pStr1 = "abcd";
	char *pStr2 = "wert";
	cout<<Compare(pStr1, pStr2)<<endl;
	system("pause");
	return 0;
}

在这里插入图片描述
函数模板不能解决一些问题的时候,我们引入了模板函数特化模板函数特化形式如下:

1)关键字template后面接一对空的尖括号<> 。
2)函数名后接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参。
3)函数形参表
4)函数体

template<>
返回值 函数名<Type>(参数列表)
{
	//函数体
}

注意: 在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不匹配,编译器将为实参模板定义中实例化一个实例。

#include <iostream>
using namespace std;
template<typename T>
T Compare(const T left, const T  right)
{
	return left > right ? left : right;
}
template<>
char const *Compare<const char*>(const char *const left, const char *const right)
{
	if (strcmp(left, right) > 0)
	{
		return left;
	}
	return right;}
int main()
{
	const char *const str1 = "abcde";
	const char *const str2 = "werty";	
	cout << Compare(str1, str2)<<endl;
	char* const str3 = "abcd";
	char* const str4 = "wwer";
	cout << Compare(str3, str4)<<endl;
	system("pause");
	return 0;
}

注意特化不能出现在模板实例的调用之后,应该在头文件中包含模板特化的声明,然 后使用该特化版本的每个源文件包含该头文件。

十、函数模板的总结

【优点】 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。 增强了代码的灵活性。

【缺点】模板让代码变得凌乱复杂,不易维护,编译代码时间变长。 出现模板编译错误时,错误信息非常凌乱,不易定位错误

十一、C++模板与仿函数实现冒泡排序

相信很多人对冒泡排序已经了如指掌了,所以我也就不做原理展示了,那么实现多种类型的排序呢?你可能第一个想到的是库函数qsort,没错,这个函数确实可以实现不同类型的排序,那么如果说我在此基础上还想再输出的时候控制其升序降序,恐怕一个qsort是远远不够的。

1)模板函数、模板类、仿函数

参考博客:

模板类介绍
模板类定义及实例化

今天所用到的正是C++中的模板函数、模板类以及仿函数的结合。何为仿函数它是使一个类的使用看上去像一个函数,在C++中,我们通过在一个类中重载()运算符的方法使用一个函数对象而不是一个普通函数。这样我们在输出的时候就可以通过仿函数附加的功能对数组内的元素进行升序与降序的控制了。

源代码及注释

#include <iostream>
using namespace std;
template<class T>
class Greate
{
public:
	bool operator()(const T& left, const T& right)//仿函数,重载()
	{
		return left > right;//升序
	}
};
template<class T>
class Less
{
public:
	bool operator()(const T& left, const T& right)//仿函数,重载()
	{
		return left < right;//降序
	}
};
template<class T,class Compare>
void BubbleSort(T *_array, size_t _size)
{ 
	for (size_t idx = 0; idx < _size - 1; idx++)
	{
		bool iState = false;//初始化标志位
		for (size_t jdx = 0; jdx < _size - idx - 1; jdx++)
		{
			//调用Compare(),判断排序是升还是降
			if (Compare()(_array[jdx], _array[jdx + 1]))
			{
				iState = true;//交换时改变标志位的状态
				std::swap(_array[jdx], _array[jdx + 1]);
			}
		}
		if (!iState)//若标志位为false,说明未发生交换
			break;
	}
}
int main()
{
	int _array1[] = { 3,2,7,8,1,9,0,11,23,4 };
	char _array2[] = { 'c','w','y','m','d','s','q','o','a','b' };
	BubbleSort<int, Greate<int>>(_array1,sizeof(_array1)/sizeof(_array1[0]));//升序int
	BubbleSort<int, Less<int>>(_array1, sizeof(_array1) / sizeof(_array1[0]));//降序int
	BubbleSort<char, Greate<char>>(_array2, sizeof(_array2) / sizeof(_array2[0]));//升序char
	BubbleSort<char, Less<char>>(_array2, sizeof(_array2) / sizeof(_array2[0]));//降序char
	system("pause");
	return 0;
}

十二、结语

努力并不一定成功,放弃则一定会是失败,且行且努力!!!

猜你喜欢

转载自blog.csdn.net/qq_41035588/article/details/84309211