函数模板与类模板

本文详细介绍了C++中的函数模板和类模板,包括它们的区别、实例化、实参推演、特例化、非类型参数、默认值、接收模板返回值、重载以及模板在类中的应用。同时,探讨了模板特例化的完全特例化和部分特例化,并讲解了拷贝构造函数模板和普通函数、函数模板、函数模板特例化在模板重载中的作用。文章还提醒读者注意模板使用中的注意事项,如模板的默认值需要C++11支持,以及模板的非类型参数限制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数模板与类模板

我们在前面的博客中学习过了函数重载,现在则需要学习一下模板,模板相对于函数重载有着特有的优势,我们举例说明。

之前我们学习函数重载的时候,举了一个例子,就是将传入的两个值相加,并进行输出。

代码如下:

 

当时我们为了传入不同类型的数据,比如double类型的数据,我们使用了函数重载。

代码如下:

 

如果还要传入char类型的数据,则还要再进行编写,而这些Sum函数除了处理的数据类型不同之外,其他都是一样的,那么能否我们只写一个Sum函数,就可以处理传入的所有数据类型呢?

答案是可以的,这就引出了模板,比如现实中的“模子”。

 

模板分为两种:1.函数模板 2.类模板

我们这里依次来介绍:

一:函数模板

①:函数模板与模板函数的区别

函数模板是一个模板,如下面代码:

而模板函数则就是一个函数了,是通过在调用点传入模板类型到虚假模板类性参数列表里,然后函数模板根据这个传入的模板类型就可以实例化一份函数,这个函数就叫做模板函数。

 

模板的作用:就是将类型参数化

如我们传入int类型,则会实例化一份下面的函数:

注意:模板体在编译阶段是不编译的,在调用点才编译,所以我们调试模板的时候,要将所有模板都调用一遍。

 

例如:

我们可以发现,编译器没有报错,因为编译阶段模板体是没有编译的,只有当我们调用这个模板,则这个模板才会进行编译,才会发现这个错误。

 

我们调用一下这个模板,再看看结果:

这时候就会发现,编译器报错了,它发现这个错误了。

 

②:模板的实例化

模板实例化:通过传入的类型参数替换掉模板类型参数,是typedef的过程

注意:同一种类型,模板只会实例化一次,之前实例化过,就直接调用即可。

 

③:模板的实参推演

例如:

这两种方式都是可以的。

推演过程:在调用点没有模板实例化需要的类型参数,但是通过判断传入的两个实参都是整形int类型的,而且函数模板需要传入的两个类型参数是一致的,则自动推演出来传入整形int类型参数。

模板的实参推演的限制:

  • 不能让编译器产生二义性
  • 要有实参

如果给定一部分类型,另外一部分类型也可以进行实参推演:

 

④:模板的特例化

模板的特例化:当模板不能满足特殊类型的需求,就需要使用模板的特例化。

例如:

这种情况下,我们无法对这两个字符串进行相加,那么我们只能提出特例化的版本了。

模板的特例化分为两种:

  • 完全特例化(全特化) 应用于:函数模板&类模板
  • 部分特例化(偏特化) 应用于:类模板

 

我们分来来看:

⑴:完全特例化版本

//模板的特例化(模板不能满足特殊类型的需求) 全特化
template<typename T>			//函数模板
T Sum(const T a, const T b)
{
	std::cout << "template<typename T> void Show()" << std::endl;
	std::cout << "type:" << typeid(T).name() << std::endl;
	return a + b;
}

template<>
char* Sum<char*>(char* const pa, char* const pb)//函数模板全特化<char*>
{
	std::cout << "template<> void Show()" << std::endl;
	std::cout << "type:" << typeid(pa).name() << std::endl;
	int len = strlen(pa) + strlen(pb) + 1;
	char *pnewstr = new char[len]();		//重新开辟一个足够大小的空间
	strcpy(pnewstr, pa);				//先拷贝hello
	strcat(pnewstr, pb);				//接着拷贝world 变成helloworld
	return pnewstr;						//将这个空间地址返回
}

int main()
{
	Sum<int>(10, 20);
	Sum<double>(10.1, 10.2);
	char* prt = Sum<char*>("hello", "world");
	std::cout << prt << std::endl;          //打印helloworld
	delete[] prt;							//释放此空间,防止内存泄漏
	return 0;
}

我们看一看运行结果:

我们可以看到,运行的是char*类型的完全特例化版本,并且打印结果正确。

 

⑵:部分特例化版本

因为偏特化不能用于函数模板,只能用于类模板,所以我们用类模板来测试:

//模板的特例化(模板不能满足特殊类型的需求) 偏特化
template<typename T>
class CSum
{
public:
	T Sum(T a, T b)
	{
		std::cout << "1" << std::endl;
		return a + b;
	}
};

template<typename T>
class CSum<T*>
{
public:
	T Sum(T* a, T* b)
	{
		std::cout << "2" << std::endl;
		return *a + *b;
	}
};

int main()
{
	CSum<int> cs1;
	cs1.Sum(10, 20);

	int a = 10;
	int b = 20;
	CSum<int*> cs2;
	cs2.Sum(&a, &b);

	double a1 = 10.1;
	double a2 = 10.2;
	CSum<double*> cs3;
	cs3.Sum(&a1, &a2);
	return 0;
}

我们看一看运行结果:

我们可以看到,除了第一种运行的是类模板的普通版本,其他两个都运行的是类模板的偏特化版本。

注意事项:

  • 特例化的版本优先级 > 普通模板的版本(类似于有现成的饭,就不必自己做了)
  • 特例化的版本是基于普通模板版本的,不能只有特例化的版本而没有普通模板版本。
  • 特例化的版本要和普通模板版本的逻辑一致。

 

⑤:模板的非类型参数

例如:

//模板的非类型参数
template<typename T>//这里模板只有类型参数T
void Sort(T arr[], int len)//冒泡排序
{
	T tmp = T();//对tmp进行O初始化
	for (int i = 0; i < len - 1; i++)
	{
		for (int j = 0; j < len - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

template<typename T, int len>//这里模板有类型参数T,以及非类型参数int len
void Sort(T arr[])//冒泡排序
{
	T tmp = T();//对tmp进行O初始化
	for (int i = 0; i < len - 1; i++)
	{
		for (int j = 0; j < len - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

template<typename T>
void Show(T arr[], int len)//进行简单打印
{
	for (int i = 0; i < len; i++)
	{
		std::cout << arr[i] << " ";
	}
	std::cout << std::endl;
}

int main()
{
	int arr[] = { 123, 123, 214, 32, 423, 421, 2 };
	//int len = sizeof(arr) / sizeof(arr[0]);//局部变量不能用作非类型参数
	const int len = sizeof(arr) / sizeof(arr[0]);//替换成常量即可
	Show(arr, len);//先打印一遍
	Sort<int, len>(arr);//局部变量不能用作非类型参数
	Show(arr, len);//排序后,再打印一遍。
	return 0;
}

我们看一看运行结果:

我们可以看到,结果和预想的一致,且非类型参数需要常量进行传参。

注意事项:

  • 模板的类型参数列表中的非类型参数,不能为double和float,原因类似于switch,浮点值存在一个范围EPS,只要值与0差值不大于EPS,则也当作0处理,所以有可能有多个值指向同一个值,所以不可以为浮点值。
  • 模板的类型参数列表中的非类型参数,要用常量(常属性)赋值,不能用变量赋值。

 

⑥:模板的默认值

代码如下:

//模板的默认值
template<typename FT, typename ST, typename RT = int>
RT  Sum(FT first, ST second)
{
	std::cout << typeid(ST).name() << std::endl;
	return first + second;
}

int main()
{
	Sum<int, double, int>(10, 20.1);
	Sum<double, int>(10.1, 20.2);
	Sum<int,int>(10.1, 10);

	return 0;
}

编译器运行失败的原因:函数模板的默认值 需要 C++11版本支持,而我这台笔记本安装的vs2012对于C++11只是部分支持,对于这部分是没有支持的,所以会进行报错。

 

⑦:接收模板不明确的返回值

例如:

//接收模板不明确的返回值
template<typename FT, typename ST, typename RT>//RT作为返回值类型参数
RT  Sum(FT first, ST second)				//我们不知道返回的数据具体类型
{
	std::cout << typeid(ST).name() << std::endl;
	return first + second; 
} 

int main()
{
	auto rt = Sum<int, int, int>(10, 20);//我们在调用点传入类型参数的时候才能知道返回的数据具体类型
										//所以这里我们还不知道用什么类型的变量来接收,所以用auto关键字让其自适应
	std::cout << typeid(rt).name() << std::endl;
	return 0;
}

我们看一看运行结果:

我们通过结果发现,可以通过关键字auto自适应来接收模板的不明确类型的返回值

 

⑧:模板的重载

例如:

//模板的重载
bool Compare(char* a, char* b)//普通函数版本
{
	std::cout << "bool Compare(char* ,char*)" << std::endl;
	return strcmp(a, b) > 0;
}


template<typename T>//普通模板版本
bool Compare(T a, T b)
{
	std::cout << "template<typename T> bool Compare(T ,T)" << std::endl;
	return a > b;
}

//bool __cdecl Compare<char *>(char *,char *)
template<>				//模板的完全特例化版本
bool Compare<char*>(char* a, char* b)
{
	std::cout << "template<> bool Compare<char*>(char* ,char*)" << std::endl;
	return strcmp(a, b) > 0;
}

int main()
{
	char* p = "hello";
	char* q = "world";
	Compare(p, q);
	Compare("hello","world");
	return 0;
}

我们看一看运行结果:

我们可以发现:

  • 普通函数版本①,普通模板版本②,模板的特例化版本③,这三者构成了模板的重载。
  • 三者的优先级是:① > ③ > ②
    • 之所以运行结果是这样,则是因为“hello”和“world”的类型是const int*,而不是int*。

注意:首先查看哪些普通函数是可行的(确定这个普通函数是否可行,必须是传入实参和函数形参类型的精确匹配,经过类型转换匹配的不符合要求),然后才是查看模板的特例化是否可行(也是要精确匹配),都匹配不上,才由普通模板进行推演。

 

⑨:模板的显示实例化

我们在一个文件下只写了模板的声明,而在另一个文件下进行了该模板的定义,那么直接调用是否能够成功呢?

代码如下:

//main.cpp
//模板的显示实例化
template<typename T>
T Sum(T, T);//Sum    //Sum<int>  *UND*

int main()
{
	std::cout  << Sum<int>(10, 20) << std::endl;
	std::cout  << Sum<double>(10.1, 20.2) << std::endl;
	return 0;
}

 

//test.cpp
template<typename T>
T Sum(T a, T b)
{
	return a + b;
}

//template int Sum(int, int);//int Sum<int>(int,int);方法①
//告诉编译器,定义好之后,直接生成一份int Sum<int>(int,int)的模板函数
//int rt = Sum<int>(10, 20);//int Sum<int>(int,int)方法②
//方法①②两者意义相同。

//double rt = Sum<double>(10.0, 20.0);//与上面意义一致
//template double Sum(double, double);

运行结果如下:

我们会发现,运行结果报错,果然出现了符号解析错误,原因自然是因为模板体编译阶段是不编译的,所以我们定义完模板之后要先实例化一次,让其模板函数生成。

 

我们修改之后,再次运行,修改后代码以及运行结果如下:

我们可以看到,运行成功,结果和预想的一致。

 

建议:我们发现每一个类型就得显式实例化一次,太过于麻烦,所以我们平常应用时,一般将其设计在.h文件下(头文件)

 

 

二:类模板

①:模板名称 + 模板的类型参数列表 = 类型名称(类名)

例如:

我们可以发现:模板名称 + 模板的类型参数列表 = 类型名称(类名),那么在调用点实例化的时候,真正的类名已经是Node<int>了。

而这里我们有一点建议:一般在构造函数以及析构函数名称后面,我们不加模板类型参数,其余的地方都加上。

 

②:选择性实例化(在调用点没调用的方法不会进行实例化)

类模板的选择性实例化:能编译不代表没错误(可能没有调用进行编译)

所以这里有一点建议:测试模板代码功能时,将每一处都调用一遍,保证其正确性。

证明如下:

 

③:类模板中的方法如何在类外实现时,以及typename关键字的作用

类模板中的方法类外实现时,需要加模板声明

(1):如果函数返回值是编译器自带类型

例如:

那么Show函数应该这样在类外实现:

 

(2):如果函数返回值是自定义类型

例如:

那么Find函数应该这样在类外实现:

这时,我们可以总结一下typename关键字的作用:

  • 定义模板类型参数
  • 声明模板中的一个类型,如上图中的typename CLink<T>::Node* CLink<T>::Find(T val){}

 

④:类模板进行友元声明时,分两种情况:一对一 和 一对多

第一种情况:一对一

例如下图:

例如下面图中红框框:

我们这里可以认为:红框框中表示友元的CLink是一个模板,那么如果实例化时传入的模板类型参数为int,系统自动将T替换为int后,则class CLink<int>是自身class CLink<int>的友元。

当然,如果将红框框中代码修改为friend class CLink<double>,则class CLink<double>是自身class CLink<int>的友元。

 

第二种情况:一对多

例如下图:

例如下面图中红框框:

我们这里可以认为:红框框中表示友元的CLink是一个类,则不论实例化时传入的模板类型参数是什么,都是自身class CLink<int>的友元。

 

⑤:拷贝构造函数模板会退化为 普通的构造函数

我们如果要用模板类型参数为int生成的一份模板类对象 来拷贝生成 另一份模板类型参数为double的模板类对象,那么如何实现呢?

我们这里首先要分清拷贝构造函数 和 拷贝构造函数模板的区别:

  • 拷贝构造函数:    用已知类型对象 来生成 相同类型的新对象
  • 拷贝构造函数模板:用已知类型对象 来生成 未知类型的新对象

 

我们这里要调用的拷贝构造函数模板代码如下:

但是我们可以需要知道的是:拷贝构造函数模板 会退化成 构造函数,那么这时我们就相当于没有提供拷贝构造函数,那么当已存在的对象生成相同类型的新对象的时候就会调用系统提供的默认拷贝构造函数,我们都知道系统提供的默认拷贝构造函数是一个浅拷贝,可能会使一块内存被重复释放。

 

所以我们自己一定要实现拷贝构造函数,实现一份深拷贝的,代码如下:

 

我们可以调用测试一番,看运行结果是否正确:

我们可以看到:

  • 第一行:生成一份模板类型参数为int的对象,调用的是构造函数的模板,正确。
  • 第二行:用已生成的对象ilink来生成一份模板类型参数为double的新对象,调用的是拷贝构造函数模板(已退化为构造函数),正确。
  • 第三行:用已生成的对象ilink来生成一份模板类型参数依旧为int的相同类型新对象,调用的是拷贝构造函数,没有调用拷贝构造函数模板,正确。

 

⑥:模板中的方法只是一个普通的方法,无法直接提供特例化的版本

当类模板中只有一两个类型不支持我们的某个函数,那么我们只需要给这个普通函数先提供一份模板版本,再对这个模板版本进行特例化即可。

 

比如我们在类模板中的一个普通的方法Find,代码如下:

我们发现,当需要查找char类型数据的时候,红框框中的比较方法则不适用了,所以我们得提供一份支持查找char类型数据的版本,这时我们就想到了模板的完全特例化了,但是我们也要知道,模板的特例化是依赖于模板的,不能在没有模板的版本下,直接提供模板的特例化版本,所以我们的代码如下:

 

首先提供一个函数Find的模板版本:

 

在此基础上,再提供一份函数Find的模板特例化版本:

我们发现,我们将红框框中的代码进行了修改,使其可以对比查找char类型的数据。

最终,普通函数,函数模板,函数模板的特例化,这三者构成了我们模板的重载。

 

⑤和⑥的完整代码如下:(博客中有)

⑤完整代码:

template<typename T>
class CLink //模板名称
{
public:
	CLink();
	//CLink(const CLink<T>& rhs);
	~CLink();
	bool InsertTail(T val);
	void Show();
	class Node;
	Node* Find(T val);
private:
	class Node//类名称
	{
	public:
		Node(T val = T());
	public:
		T mdata;
		Node* pnext;
	};
	Node* phead;
};
template<typename T>
CLink<T>::Node::Node(T val) :mdata(val), pnext(NULL){}

template<typename T>
CLink<T>::CLink()
{
	phead = new Node();
}

template<typename T>
CLink<T>::~CLink()
{
	Node* pCur = phead;
	Node* pNext = pCur;
	while (pCur != NULL)
	{
		pNext = pCur->pnext;
		delete pCur;
		pCur = pNext;
	}
	phead = NULL;
}

template<typename T>
bool CLink<T>::InsertTail(T val)
{
	Node* pnewnode = new Node(val);
	Node* ptail = phead;
	while (ptail->pnext != NULL)
	{
		ptail = ptail->pnext;
	}

	ptail->pnext = pnewnode;
	return true;
}

template<typename T>
void CLink<T>::Show()
{
	Node* pCur = phead->pnext;
	while (pCur != NULL)
	{
		std::cout << pCur->mdata << " ";
		pCur = pCur->pnext;
	}
	std::cout << std::endl;
}

template<typename T>
typename CLink<T>::Node* CLink<T>::Find(T val)
{
	Node* prt = phead->pnext;
	while (prt != NULL)
	{
		if (prt->mdata == val)
		{
			break;
		}
		prt = prt->pnext;
	}
	return prt;
}

int main()
{
	CLink<int> link;
	for (int i = 0; i < 10; i++)
	{
		link.InsertTail(i + 1);
	}
	link.Show();
	link.Find(10);
	return 0;
}

 

⑥完整代码:

template<typename T>
class CLink
{
public:
	CLink()
	{
		std::cout << "template<typename T> class CLink" << std::endl;
		phead = new Node();
	}
	CLink(const CLink<T>& rhs)
	{
		std::cout << "CLink(const CLink<T>& rhs)" << std::endl;
		phead = new Node();
		Node* ptail = phead;
		Node* pCur = rhs.phead->pnext;
		while (pCur != NULL)
		{
			Node* pnewnode = new Node(pCur->mdata);
			ptail->pnext = pnewnode;
			pCur = pCur->pnext;
			ptail = ptail->pnext;
		}
	}

	template<typename E>
	CLink(const CLink<E>& rhs)// 退化成构造函数
	{
		std::cout << "template<typename E> CLink(const CLink<E>& rhs)" << std::endl;
		phead = new Node();
		Node* ptail = phead;
		CLink<E>::Node* pCur = rhs.phead->pnext;
		while (pCur != NULL)
		{
			Node* pnewnode = new Node(pCur->mdata);
			ptail->pnext = pnewnode;
			pCur = pCur->pnext;
			ptail = ptail->pnext;
		}	
	}

	~CLink()
	{
		Node* pCur = phead;
		Node* pNext = pCur;
		while (pCur != NULL)
		{
			pNext = pCur->pnext;
			delete pCur;
			pCur = pNext;
		}
		phead = NULL;
	}
	bool InsertTail(T val)
	{
		Node* pnewnode = new Node(val);
		Node* ptail = phead;
		while (ptail->pnext != NULL)
		{
			ptail = ptail->pnext;
		}

		ptail->pnext = pnewnode;
		return true;
	}
	void Show()
	{
		Node* pCur = phead->pnext;
		while (pCur != NULL)
		{
			std::cout << pCur->mdata << " ";
			pCur = pCur->pnext;
		}
		std::cout << std::endl;
	}

	class Node;//前置声明
	Node* Find(T val)//普通方法
	{
		std::cout << "Node* CLink<T>::Find(T)" << std::endl;
		Node* pCur = phead->pnext;
		Node* prt = NULL;
		while (pCur != NULL)
		{
			if (pCur->mdata == val)
			{
				prt = pCur;
				break;
			}
		}
		return prt;
	}

	template<typename E>
	Node* Find(E val)  //模板版本
	{
		std::cout << "template<typename E> Node* CLink<T>::Find(E)" << std::endl;
		Node* pCur = phead->pnext;
		Node* prt = NULL;
		while (pCur != NULL)
		{
			if (pCur->mdata == val)
			{
				prt = pCur;
				break;
			}
		}
		return prt;
	}

	template<>
	Node* Find(char* val) //特例化的版本
	{
		std::cout << "template<> Node* CLink<T>::Find(char*)" << std::endl;
		Node* pCur = phead->pnext;
		Node* prt = NULL;
		while (pCur != NULL)
		{
			if (strcmp(pCur->mdata,val) == 0)
			{
				prt = pCur;
				break;
			}
		}
		return prt;
	}
private:
	class Node
	{
	public:
		Node(T val = T()) :mdata(val), pnext(NULL){}
	public:
		T mdata;
		Node* pnext;
	};

	friend class CLink;
	Node* phead;
};

int main()
{
	CLink<int> ilink;//CLink<int>
	CLink<double> dlink(ilink);

	CLink<int> ilink1(ilink);
	return 0;
}

 

⑦:最后,这里对类模板有几点建议:

  • 在构造函数以及析构函数名称后面,我们建议不加模板类型参数,其余的地方都加上。
  • 虚假的类型也可以零初始化,例如Node<T> (T val = T())
  • 类模板会进行选择性实例化,能编译不代表没有错误。
  • 类模板的模板类型参数在调用点只能指定,不能进行推演
  • 类模板使用时,要让编译器知道它是一个类模板名称,必要时,可前置声明。
  • 模板的重载:由普通函数,函数模板,函数模板的特例化,这三者构成。

typename的两个作用:

  • 1. 定义模板类型参数(和class功能相同)
  • 2. 声明模板中的一个类型(上面函数模板的显式实例化中用到了)

 

 

三:其他零碎知识

0初始化:

我们申请变量的时候一般会同步进行初始化,没有初始值则将其零初始化。

但是我们可以发现,传入int,double等都可以,但是T为虚假类型,则有可能为指针,那么这样子传入就会出错。

所以我们可以在数据类型后面加上“()”小括号,将其开辟的空间全部置0。

那么上述代码可以修改为:

类似于 int tmp = int();以及double tmp = double();

不过这里需要注意的是不能传带*号的指针类型进去,因为*号有二义性,系统不知道它表示的是指针还是其本身的乘号,所以我们要将其重定义:

这样子就不会使编译器产生二义性了。

 

至此,模板基本了解完毕。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值