简述模板函数和模板类


自从OO(Object-Oriented)出现之后,随着思想模型的逐步确立和SE开发的需要,泛型编程开始并逐渐成为一种重要的编码手段。

使用泛型的好处是,可以将源程序的具体行为和具体的类型无关,最终的类型确定要到最后阶段的绑定。这有点像类中的多态。

而模板是泛型编程的基础,很难想象没有模板的泛型会成为啥样子。

本文旨在帮助你了解熟悉并掌握如何编写自己简单的模板函数和模板类

1.模板是什么

其实,如过你本身对C++没有什么了解,我很难告诉你什么是模板……

模板在语言中的意思大致是“生产一类具有相同功能的模子和器具”,比如民间传统手工作品中制作泥塑的模子,当你把一坨像shit一样的泥巴放入泥塑模子中,然后经过一堆工序,最终出来的就是特定的泥塑作品了。而C++中的模板也有如此的作用。

如果你用过STL中的容器,就可能会接触过vector/deque/lis/map等容器,他们都是利用模板特性设计的。而这些容器的好处是,好多好多的类型都可以通吃而不会产生消化不良的症状。

2. 为什么要用模板

这是一个很有趣的问题,也是一个很严肃的问题。

在还是C的时代,数据结构重用是很让程序员头疼的一个问题,比如你写了一个以int为操作类型的堆栈,那么这个堆栈是不能用来操作char甚至是double(不考虑类型转换)。如果你需要操作其他类型的堆栈,那么基本上只能重写一次了。

所以通常Coder们都用ElementType来代替具体的类型,然后在使用这个数据结构的文件的某处写上typedef int ElementType

俗话说,躲得过初一,躲不过十五,于是新的问题随之而来,这样的做法不能在一个文件中同时对两种数据类型生效。

而如果你是在C++中写堆栈类,那么你可以运用模板,然后使用Cstack,Cstack来定义不同数据类型的堆栈对象。

这样的优点是显而易见的。

3. 如何使用模板

3.1使用模板函数

我们可以单独对的一个函数使用模板,使其具有模板特性,比如我们要写一个Swap模板函数,希望这个函数能够交换几乎所有类型的两个变量。

那么我们可以这样写:

template<typename T> void Swap(T& a, T& b);
int _tmain(int argc, _TCHAR* argv[])
{
	int nA = 5, nB = 3;
	wstring sA = L"I'm a boy", sB = L"I'm a girl";

	wcout<<L"before the swap :"<<nA<<L" "<<nB<<L" "<<sA<<L" "<<sB<<endl;
	Swap(nA, nB);
	Swap(sA, sB);
	wcout<<L"after the swap :"<<nA<<L" "<<nB<<L" "<<sA<<L" "<<sB<<endl;

	_getch();
	return 0;
}
template<typename T>
void Swap(T& a, T& b)
{
	T tmp = a;
	a = b;
	b = tmp;
}

模板定义从关键字template开始,后面的指明了模板形参,表明T是模板类型。

在某些地方,这里用的是class而不是typename,其实二者作用是一样的。只不过后者更符合模板的风格,而前者的存在是因为当初的很多编译器只支持class关键字

声明了T为模板形参后,你可以把T当作一种新的类型,而且这种类型具有容器效果,可以容下其他几乎所有的类型。

模板类型的好处是,类型的实例化是最后编译阶段确定的,上面的例子中,两个Swap可以看成两个不同的函数

当然,模板形参可以有多个,这点无论在模板函数还是模板类中都是成立的,但是每个模板类型前面都要显示的加上typename,即

template<typename T, typename Global>

而且,模板形参中不一定非要都是模板类型,也可以是内建的类型,只要这种类型在编译时刻编译器能够确定其类型即可。

这里还要注意的是,如果在函数的返回类型中使用模板类型形参,那么它必须由调用者显示的指定,而且,该返回类型的模板类型要在参数列表的首位,因为模板参数的类型是逐一顺序匹配的

template<typename RetType, typename Input> RetType Sum (const Input& a, const Input& b);
int _tmain(int argc, _TCHAR* argv[])
{
	double a = 0.25, b = 1.55;
	int nRet = Sum<int>(a,b);

	wcout<<L"the result of 0.25 plus 1.55 is "<<nRet<<endl;// the result is 1

	_getch();
	return 0;
}
template<typename RetType, typename Input>
RetType Sum (const Input& a, const Input& b)
{
	return (a + b);
}

3.2使用模板类

从本质上说,使用模板类和使用模板函数没有什么不同,但是就细节而言,仍存在一些细细微微,甚至让人感到无所适从的问题。

我们假设要写一个Csort类,这个类具有冒泡排序和插入排序的功能。当然,这个类是模板类,因为我们的排序类型是普适的~

// in the .h
template<typename T>
class CSort
{
	public:
		//CSort();
		//~CSort();
		void BubbleSort(T a[], int nCount);
		void InsertionSort(T a[], int nCount);
	protected:
		void Swap(T& a, T& b);
	private:
};

template<typename T>
void CSort<T>::Swap(T& a, T& b)
{
	T tmp = a;
	a = b;
	b = tmp;
}

template<typename T>
void CSort<T>::BubbleSort(T a[], int nCount)
{
	for (int n = 1; n < nCount; ++n)
	{
		int nUpbound = nCount - n - 1;

		for (int j = 0; j <= nUpbound; ++j)
		{
			if (a[j] > a[j+1])
			{
				Swap(a[j], a[j+1]);
			}
		}
	}
}

template<typename T>
void CSort<T>::InsertionSort(T a[], int nCount)
{
	for (int n = 1; n < nCount; ++n)
	{
		T nTmp = a[n];
		int nIndex = n - 1;

		while (nIndex >= 0 && nTmp < a[nIndex])
		{
			a[nIndex + 1] = a[nIndex];
			--nIndex;
		}

		a[nIndex + 1] = nTmp;
	}
}

到这里,你可能会有疑问,对!你没有看错,类的声明和实现都直接在.h文件中,至于原因是什么,后面说~

类模板首先要在类声明的前面声明模板形参,而且每个成员函数,无论是共有还是私有还是保护,都要在实现前面加上模板形参的声明。

并且还要在函数类名限定域后面加上模板形参类型,比如CSort::XX

在前面的例子中,我把构造和析构给注释了,原因很简单,如果显示声明了构造和析构,那么就一定要实现它,因为需要为函数实现前面加上模板形参等东西,而如果不显示这么做,则编译器会自己完成,所以,这么做纯粹是为了偷懒TvT

至于模板类的使用,如下面代码所示(略去了头文件):

int _tmain(int argc, _TCHAR* argv[])
{
	int Arr[] = {2, 3, 1, 4, 8, 9};
	double dbArr[] = {1.2, 2.4, 9.2, 5.7, 3.14, 4.87};
	CSort<int> IntSort;
	CSort<double> dbSort;

	IntSort.BubbleSort(Arr, _countof(Arr));
	dbSort.InsertionSort(dbArr, _countof(dbArr));

	for (int n = 0; n <= 5; ++n)
	{
		wcout<<Arr[n]<<L"\t"<<dbArr[n]<<endl;
	}

	_getch();
	return 0;
}

使用模板类,我们需要显示的指定这个类的类型,如上面的CSort,这种做法相当于给模板类进行确定类型绑定的过程。

如果指定了int,那么类声明和实现中模板类型T则被int绑定

具有相同绑定类型的模板类可以看成一种类,比如

CSort<int> a,b;
CSort<double> c, d;

这里a和b可以看作一种CSort类,c和d则是另一种CSort类。

这在理解模板类的static成员变量和函数有着很重要的作用

由前面的只是可以得到,类的static成员变量是所有类对象共有的,而不是某一个类对象所有的,那么对应到模板类中,则是具有相同绑定类型的类共有的。

假设在上面的CSort中,有一个static变量var,那么,a和b共享一个var,c和d共享另外一个var。

而要使用模板类的static成员函数,则需要指明绑定对象或者直接依靠对象使用。

假设CSort有一个static函数fun,那么应该如此使用:

CSort<char> a;
CSort<int>::fun
CSort<double>::fun
a::fun;

3.3如何编译模板类

由于模板类型不是一种实类型,他必须等到后期进行类型绑定之后才能确定最终类型,所以在使用的地方必须要能够让编译器“看到”使用模板的地方,才能正常的顺利的产生编译代码。

说了这么多,其实就是说,常规的以前的,将类声明写在.h实现写在cpp中,然后#inlcude .h的方式是行不通的。因为#inlcude的地方看不到cpp

所以通常的解决的方法是,将实现直接写在.h中,这也是STL/WTL等库的做法。

呃,什么,你担心内联导致代码膨胀?呃,这的确是一个问题,用WTL的确会出现代码剧烈膨胀,不过似乎没有想象的那么严重。

第二种是按照以前的做法,声明归声明,实现归实现,然后在原来需要inlcude头文件的的地方改成#include”xx.cpp”

这样做的问题是,某些编译器不支持,M$的支持,G++似乎不支持。第二会带来重复编译的情况,导致编译性能显著下降,所以WTL/STL都没有采取这种做法

第三种是传说中的方法,即在使用类模板的地方加上export。这是最理想的做法,但是由于太过理想,所以只能在传说中存在。

其实意思就是,目前还没有编译器支持这种做法,当你用export指定的时候,编译器给出的信息是,尚未实现此关键字,但保留,未来会做更新云云

所以还是现实点,用前面两种吧

, , , ,

  1. No comments yet.
(will not be published)