C++——函数模板与类模板

0.关注博主有更多知识

C++知识合集

目录

1.泛型编程

2.函数模板

2.1函数模板实例化

2.2函数模板参数的匹配原则

3.类模板

4.模板的分离编译

1.泛型编程

实际上泛型编程的难度是比较高的,但我们泛型编程的初学者,当然要从简单的地方开始入手。

我们可以写出很多份交换函数,这些函数之间构成函数重载,这样在调用的时候就能自动调用不同参数类型的函数:

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

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

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

/*......*/

那么像上面这样写能不能解决我的需求呢?能!但是太挫了,原因有两个

  1.重载的函数仅仅是类型不同,函数体内容都是一样的,代码复用的率比较低;只要有新的类型出现,就需要增加对应的函数

  2.代码的可维护性比较低,一个函数出错可能所有的重载都会出错

那么我们可以试着摸一摸泛型编程的门道,向上面的Swap()函数一样,这几个Swap()函数除了参数类型不一样,其他的大部分内容都是一样,那么是不是可以将这些相同的地方集合起来,组成一个模板,然后让编译器根据不同的类型来利用该模板自动生成代码呢?

事实上在C++当中,确实存在上述这样的模板,通过给这个模板提供不同的参数类型,就可以获得不同的具体类型代码。那么泛型编程就是编写与类型无关的通用代码,是代码复用的一种手段,模板是泛型编程的基础。

2.函数模板

函数模板代表了一个函数家族,该函数模板与类型无关,在是使用时被参数化,根据实参类型产生函数的特定类型版本。也就是说函数模板不是一个具体的函数,在调用函数模板时会根据调用函数的实参生成一份特定的类型版本代码。我们以上面的Swap()函数为例:

template <class T>
void Swap(T &left, T &right)
{
	T tmp = left;
	left = right;
	right = tmp;
}

int main()
{
	int x = 3, y = 5;
	Swap(x, y);

	char chl = 'a', chr = 'b';
	Swap(chl, chr);

	double d = 1.1, b = 4.3;
	Swap(d, b);
	return 0;
}

函数模板的格式为:

template <class T1,class T2,......,class Tn>
返回类型 函数名(参数列表)
{}

其中,"template"是定义模板时的关键字,其后跟上一堆尖括号,尖括号里面的内容就是模板的参数,需要注意,模板的参数与函数的参数不一样,模板的参数是定义类型,而函数的参数是定义指定类型的变量。其中模板参数的定义可以使用关键字class或者typename。函数模板的有效范围在"template"关键字之后碰到的第一个函数内有效

函数模板本身并不是一个函数。在编译器编译阶段,编译器需要根据调用函数模板时传入的实参来推演并实例化生成对应类型的函数以供正确调用。

例如在外部调用Swap()的两个实参都是double类型,在编译阶段,编译器会根据实参的类型,将函数模板的参数类型由T替换为double,然后再实例化出一份真正的函数代码:

2.1函数模板实例化

上面我们介绍了编译器是如何通过函数模板实例化出对应类型函数的大致流程,接下来介绍我们如何调用函数模板以及一些细节:

  1.隐式实例化:让编译器根据实参推演出函数模板的参数类型,然后再生成具体的函数

template <typename T>
T Add(const T &x, const T &y)
{
	return x + y;
}

int main()
{
	/*让编译器根据实参的类型推演出函数模板的参数类型
	 *然后再生成具体的真实函数*/
	int a = 1, b = 3;
	cout << Add(a , b) << endl;

	double c = 3.1, d = 4.6;
	cout << Add(c , d) << endl;

	return 0;
}

  用法是非常简单的,但是我们需要注意,上面程序的模板参数只有一个,即T,那么这就意味着只能确定一种类型,所以在调用函数模板时需要保证两个实参的类型相同。那么我们也知道,函数调用的时候,实参与形参之间可能发生隐式类型转换,但是我们也需要知道,这种隐式类型转换发生在函数调用当中而不在函数模板调用当中

template <typename T>
T Add(const T &x, const T &y)
{
	return x + y;
}

int main()
{
	int a = 1, b = 3;
	double c = 3.1, d = 4.6;

	/*错误调用,a的类型为int,c的类型为double
	 *函数模板只有一个参数,即只能确定一种类型
	 *而函数模板的调用不会发生隐式类型转化*/
	cout << Add(a , c) << endl;
	return 0;
}

  所以解决方案有两种,要么自己手动进行强制类型转换,要么使用显式实例化模板:

template <typename T>
T Add(const T &x, const T &y)
{
	return x + y;
}

int main()
{
	int a = 1, b = 3;
	double c = 3.1, d = 4.6;

	/*手动强制类型转换,使得实参类型统一*/
	cout << Add(a , (int)c) << endl;
	return 0;
}

  2.显式实例化:在函数模板名称后使用<>指定模板参数的类型,这时就已经实例化出了真实的函数

template <typename T>
T Add(const T &x, const T &y)
{
	return x + y;
}

int main()
{
	int a = 1, b = 3;
	double c = 3.1, d = 4.6;

	/*显式实例化,Add<int>确定了函数模板的参数类型
	 *所以即可生成对应的真实函数
	 *所以此时可以发生隐式类型转换,因为此时就是在调用真实的函数*/
	cout << Add<int>(a, c) << endl;
	return 0;
}

  此时实参与形参时间可以发生隐式类型转换,因为调用的时候不再是调用函数模板,而是调用真实的函数。如果不能发生隐式类型转化,那么将会报错。

2.2函数模板参数的匹配原则

如果函数与函数模板同时存在,那么在调用的时候优先选择函数而非函数模板

int Add(int x, int y)
{
	return x + y;
}

template <class T>
T Add(T x, T y)
{
	return x + y;
}
int main()
{
	/*此时函数模板与函数同时存在,但是这里调用例子中
	 *实参直接与函数的参数匹配,所以直接调用函数而非调用函数模板*/
	cout << Add(1, 2) << endl;
	return 0;
}

这个时候需要注意了,如果我们显式地实例化函数模板生成一份与已存在函数相同的函数,不会发生报错,并且正常调用:

int Add(int x, int y)
{
	return x + y;
}

template <class T>
T Add(T x, T y)
{
	return x + y;
}

int main()
{
	/*此时函数模板与函数同时存在,但是这里调用例子中
	*实参直接与函数的参数匹配,所以直接调用函数而非调用函数模板*/
	cout << Add(1, 2) << endl;

	/*显式实例化生成一份真实的函数
	 *看起来实例化的函数与已经存在的函数冲突
	 *实际上并没有发生冲突,还可以正常调用*/
	cout << Add<int>(1, 2) << endl;
	return 0;
}

在这个例子当中,已经存在了一份int Add(int , int),而我们显式实例化函数模板又生成了一份int Add(int , int),但是编译器没有报错,这就说明了在符号表当中,他们的函数名修饰规则一定不一样。我们在Linux环境下使用g++观察上面这个程序的汇编代码:

 

虽然说调用函数时有限使用普通函数,但如果函数模板更加匹配,那么最后调用的时候会选择函数模板

int Add(int x, int y)
{
	return x + y;
}

template <class T1,class T2>
T1 Add(T1 x, T2 y)
{
	return x + y;
}

int main()
{
	/*这两个调用都可以调用普通函数,但是要发生隐式类型转换
	 *但是上面的函数模板有两个参数,所以对应两个类型
	 *所以调用函数模板是最合适的*/
	cout << Add(1.1, 5) << endl;
	cout << Add(5, 3.4) << endl;
	return 0;
}

3.类模板

类模板的定义与函数模板的定义差不多,并且类模板不是真正的类,只有实例化出来的类才是真正的类。

我们以一个Stack类为例:

template <class T>
class Stack
{
public:
	Stack(int capacity = 4)
		:_top(0), _capacity(capacity)
	{
		_elem = new T[_capacity];
	}

	void push(const T &in)
	{
		/*不考虑扩容......*/
		_elem[_top++] = in;
	}
private:
	T *_elem;
	int _top;
	int _capacity;
};

我们Stack类作为栈,那么栈就需要存储数据,那么既然要存储数据就必然涉及到不同类型的数据,所以使用一个模板是非常有必要的。那么类模板的使用不能像函数模板那样隐式实例化,类模板的使用必须显式实例化

int main()
{
	/*实例化出不同的Stack类,用来存储不同的数据类型
	 *这几个实例化出来的类的类型是不相同的!*/
	Stack<int> st1;
	Stack<double> st2;
	Stack<char> st3;

	/*错误!他们不是相同的类型!*/
	//st1 = st2;
	return 0;
}

需要注意的是,使用不同的类型实例化出来的类,他们之间是不同的类型

4.模板的分离编译

对于函数模板来说,如果想要在一个文件下声明和定义分离是可以的,那么格式就得像下面这样:

/*函数模板的声明*/
template<class T>

T Add(const T &x, const T &y);
int main()
{
	cout << Add(1, 2) << endl;
	return 0;
}

/*函数模板的定义*/
template <class T>
T Add(const T &x, const T &y)
{
	return x + y;
}

对于类模板当中的成员函数来说,他们也是函数模板,如果这些函数模板在类模板当中声明,在类模板外定义也是可以的:

template <class T>
class Stack
{
public:
	/*类模板中只有声明*/
	Stack(int capacity = 4);
	void push(const T &in);
private:
	T *_elem;
	int _top;
	int _capacity;
};

/*声明与定义分离*/
template <class T>
Stack<T>::Stack(int capacity = 4)
:_top(0), _capacity(capacity)
{
	_elem = new T[_capacity];
}

template <class T>
void Stack<T>::push(const T &in)
{
	/*不考虑扩容......*/
	_elem[_top++] = in;
}

我们说过在类中如果只有成员函数的声明,在类外定义该成员函数时需要指明类域。上面代码当中的定义部分看起来非常奇怪,但实际上我正现在遵守刚才所说的原则,原因就在于类模板不是一个真正的类,类模板必须实例化之后才生成一个真正的类。所以在选择在类模板外部定义函数时,需要显式实例化类模板。

虽然函数模板可以在一个文件当中声明与定义分离,但是如果在多文件当中声明与定义分离编译,那么又会触发隐藏奖励:链接错误。类模板当中的成员函数也是如此:

// test.cpp
/*只有声明,定义在另一个源文件当中*/
template<class T>
T Add(const T &x, const T &y);

int main()
{
	cout << Add(1, 2) << endl;
	return 0;
}
// func.cpp
/*函数模板的定义*/
template <class T>
T Add(const T &x, const T &y)
{
	return x + y;
}

这个问题我们再熟悉不过了,原因就在于test.cpp文件当中没有Add()函数的定义,只有声明,所以编译器会认为Add()函数在其他文件当中存在,所以编译可以过;那么对于func.cpp文件来说,它也没有Add()函数的定义,因为它只有一个函数模板,并且没有人调用该模板实例化出一个函数,所以func.cpp被编译之后生成的符号表当中就没有Add()这个函数。那么test.cpp文件需要调用Add()函数但是本文件没有啊,就要发动链接器去func.cpp文件生成的符号表当中去找,但是因为func.cpp生成的符号表当中没有Add()函数,所以产生链接错误。

那么解决方法有两种,一种是在多文件分离编译中负责定义函数的文件当中显式实例化函数:

/*函数模板的定义*/
template <class T>
T Add(const T &x, const T &y)
{
	return x + y;
}

/*显式实例化*/
template int Add(const int &x, const int &y);
template double Add(const double &x, const double &y);

第二种便是模板不要在多文件当中声明和定义分离编译。第一种解决方案虽然可以正常调用函数,但是这不是一种明智的做法,因为这直接违背了泛型编程的初衷。对于类模板也一样,类模板当中的成员函数模板也不要多文件声明和定义分离编译。所以无论如何,在使用模板时,无论是函数模板还是类模板,都不要声明和定义在多文件当中分离编译

下面仅仅是演示一下类模板分离多文件分离编译也会产生链接错误:

// func.h
template <class T>
class Stack
{
public:
	/*类模板中只有声明*/
	Stack(int capacity = 4);
	void push(const T &in);
private:
	T *_elem;
	int _top;
	int _capacity;
};
// func.cpp

#include "func.h"

/*声明与定义分离*/
template <class T>
Stack<T>::Stack(int capacity = 4)
:_top(0), _capacity(capacity)
{
	_elem = new T[_capacity];
}

template <class T>
void Stack<T>::push(const T &in)
{
	/*不考虑扩容......*/
	_elem[_top++] = in;
}
// test.cpp

#include "func.h"
int main()
{
	Stack<int> st;
	st.push(1);
	return 0;
}

解决方案之一便是在定义的文件当中显式实例化:

/*声明与定义分离*/
template <class T>
Stack<T>::Stack(int capacity = 4)
:_top(0), _capacity(capacity)
{
	_elem = new T[_capacity];
}

template <class T>
void Stack<T>::push(const T &in)
{
	/*不考虑扩容......*/
	_elem[_top++] = in;
}

/*显式实例化*/
template class Stack<int>;
  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小龙向钱进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值