详解c++---array和模板

什么是array

在c语言里面使用一个括号就能申请到一段连续的空间,比如说想要申请一段空间用于存储10个整型大小的数据,就可以使用下面的代码:


void test1()
{
	int arr[10] = {0,1,2,3,4,5,6,7,8,9};
	printf("数组中下标为2的元素的值为:%d\n", arr[2]);
}

这就是一个简单的使用c语言创建数组的过程,c语言创建的数组有一个特点就是对于越界读取内容不做任何的检查,对于越界的写采用的是抽查的方式,比如说下面的代码,数组中有10个数据,但是我们却可以通过越界访问的形式查看到其他地址的内容,比如说下面的代码:

void test1()
{
	int arr[10] = {0,1,2,3,4,5,6,7,8,9};
	printf("数组中下标为2的元素的值为:%d\n", arr[2]);
	printf("越界访问的内容为:%d\n", arr[13]);
	return 0;
}

将这段代码运行一下就可以发现编译器并没有报错,能够越界访问其他地址的内容:
在这里插入图片描述
对于越界读c语言不会对其进行检查,但是对于越界写编译采用的是抽查比如说下面的代码:

void test1()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	printf("arr[12]的内容为:%d\n",arr[12]);
	arr[12]++;
	printf("修改后arr[12]的内容为:%d\n", arr[12]);
}

这段代码在一些编译器下跑的过去在一些编译器下却跑不过去,比如说vs2023就完全编译不过去:
在这里插入图片描述
但是我们将平台切换到gcc下就会发现这里编译的情况是可以通过的:
在这里插入图片描述
这里运行一下就可以发现没有报任何的错误:
在这里插入图片描述
那么这就是问题所在,同一段代码在不同的平台下运行的结果却不相同,c语言的数组发生了越界访问在有些平台下会报错,但是在有些平台下则不会报错,并且有些平台对越界检查的方式是抽查比如说arr数组中有10个元素修改arr[11]位置的数据不会报错,但是修改arr[12]位置的数据却会报错,这个平台好像是vs2013,所以c++为了解决c语言数组遗留下来的问题就创建了array这个东西,他也可以申请一段固定的空间,但是对于越界的检查是越界读不检查越界写采用断言检查,比如说下面的代码:

void test2()
{
	array<int, 5> arr = {1,2,3,4,5};
	arr[6] = 10;
	cout << "arr[6]的内容为:%d" << arr[6];
}

这段代码的运行结果为:
在这里插入图片描述
直接报了一个断言错误,然后这段代码在g++编译器下运行的结果如下:
在这里插入图片描述
我们可以看到在gcc平台下array也报了错误,那么这就是array的意义,我们来看看这个容器的接口有哪些:
在这里插入图片描述
首先我们可以看到这个容器是支持迭代器操作的比如说下面的代码:

void test3()
{
	array<int, 5> arr = { 1,2,3,4,5 };
	array<int, 5>::iterator it = arr.begin();
	while (it != arr.end())
	{
		cout << *it << " ";
		++it;
	}
}

通过array的迭代器就可以打印出容器里面的所有内容,代码的运行结果如下:
在这里插入图片描述
其次array还提供了一些函数帮助我们查询容器内部的属性:empty函数的作用就是查看当前容器的内容是否为空,size函数的作用就是查看容器的元素个数,max_size函数就是查看array容器最多能够容乃多少个数据,因为array这个函数无法修改其容量,所以max_size函数的返回值一般和size函数的返回值是一样的,比如说下面的代码:

void test4()
{
	array<int, 0> arr1;
	array<int, 5> arr2;
	if (arr1.empty())
	{
		cout << "容器arr1为空" << endl;
	}
	if (arr2.empty())
	{
		cout << "容器arr2为空" << endl;
	}
	cout << "容器arr1的数据个数为:" << arr1.size() << endl;
	cout << "容器arr2的数据个数为:" << arr2.size() << endl;
	cout << "容器arr2最多能够容乃的数据个数:" << arr2.max_size() << endl;
}

这段代码的运行结果为:
在这里插入图片描述
下面还有很多的接口,这些接口的功能和其他容器的接口功能差不多是一样的,由于array在平时的使用中用的不多,所以这里就不过多进行介绍。

非类型模板参数

模板的参数不仅可以是类型形参还可以是非类型形参,比如说array<int,10>第一个参数int表明array中的数据类型是int,第二个参数10则表明array中有10个数据,那么这个参数10就是一个非类型模板参数,在创建模板的时候可以通过class 参数名来推断参数的类型,还可以通过内置类型 参数名来接收参数的值,然后就可以在类模板里面或者函数模板里面使用这个参数比如说下面的代码:

template<class T, size_t size>
class YCF
{
public:
	void func()
	{
		cout << "T的类型为:" << typeid(T).name() << endl;
		cout << "size的值为:" << size << endl;
	}
};

int main()
{
	YCF<int,10> test1;
	test1.func();
	YCF<double, 20> test2;
	test2.func();
	return 0;
}

这段代码的运行结果如下:
在这里插入图片描述
上面的代码是在类里面使用模板参数,当然也可以在函数模板里面使用这个参数,比如说下面的代码:

template<class T, size_t size>
void func()
{
	cout << "T的类型为:" << typeid(T).name() << endl;
	cout << "size的值为:" << size << endl;
}

int main()
{
	func<int,30>();
	func<double, 20>();
	return 0;
}

这段代码的运行结果如下:
在这里插入图片描述
这里有三个小点需要大家注意一下:
第一点
浮点数,类对象以及字符串是不允许作为非类型模板参数的,非类型模板参数只能是整型常量,比如说下面的代码

template<class T,double x>
void func1()
{
	cout << "T的类型为:" << typeid(T).name() << endl;
	cout << "x的值为:" << x << endl;
}

int main()
{
	func1<int,30>();
	func<double, 20>();
	return 0;
}

这里的非类型模板参数是一个浮点型,由于模板不支持浮点型的参数,所以这段代码是运行不成功的:
在这里插入图片描述
第二点:
非类型模板参数必须得在编译期间就能确定结果,这句话的意思就是在模板实例化的时候不能传递变量给非类型模板参数只能传递常量,比如说下面的代码:

template<class T, size_t size>
void func()
{
	cout << "T的类型为:" << typeid(T).name() << endl;
	cout << "size的值为:" << size << endl;
}


int main()
{
	int x = 20;
	func<double, x>();
	return 0;
}

这里将变量x用于显示实例化模板的对象,那么结果很明显是不对的,所以这段代码的运行结果是错误的:
在这里插入图片描述
第三点:
模板的参数是可以传递缺省值的,比如说下面的代码:

template<class T= int, size_t size=10>
void func()
{
	cout << "T的类型为:" << typeid(T).name() << endl;
	cout << "size的值为:" << size << endl;
}
int main()
{
	func();
	return 0;
}

这里就给类型参数T一个缺省类型int,给非类型模板参数 size一个缺省值10,那么在使用这个模板的时候我们就可以不显示实例化直接使用这个函数,代码的运行的结果如下:
在这里插入图片描述
由于没有给模板参数,所以这里打印出来的结果就是缺省类型和缺省参数的值。

函数模板的特化

函数模板的特化从名称上来看就是对模板进行特殊化处理,所以在了解如何特化之前我们先来看看为什么会有函数的特化。

为什么会有特化

通常情况下,使用模板可以实现一些与类型无关的代码,但是对于一些特殊的类型可能会得到一些错误的结果,比如说我们创建一个less函数,为了方便以后的使用我们给这个函数加上一个模板,比如说下面的代码:

template<class T>
bool less(const T& x, const T& y)
{
	return x < y;
}

有了这个模板函数我们就可以比较很多相同类型的数据,比如说下面的测试代码:

int main()
{
	int x = 10;
	int y = 20;
	if (YCF::less(x, y))
	{
		cout << "较小的数据为:" << x << endl;
	}
	else
	{
		cout << "较小的数据为:" << y << endl;
	}
	return 0;
}

因为x的值为10y的值为20,所以这段代码的运行结果应该是:较小的数据为10,代码的运行结果如下:
在这里插入图片描述
运行的结果和我们想的是一样的,但是我们将测试代码进行一下修改,创建两个指针变量,指针变量里面记录的是两个变量的地址,然后再使用less函数对这两个指针变量进行比较,比如说下面的代码:

int main()
{
	int x = 10;
	int y = 20;
	int* px = &x;
	int* py = &y;
	if (YCF::less(px, py))
	{
		cout << "较小的数据为:" << x << endl;
	}
	else
	{
		cout << "较小的数据为:" << y << endl;
	}
	return 0;
}

这段代码的意思是比较两个指针指向的对象的大小,但是将这段代码运行的结果却并不符合我们的预期:
在这里插入图片描述
这里显示较小的数据是20那这是为什么呢?原因很简单因为在模板函数里面会将类型识别成指针,而在函数体里面进行比较的时候,比较的是指针本身而不是指针比较的对象,因为x的地址比y的地址小,所以这里运行的结果就是较小的数据为20,那么这就是上述代码的问题所在,对于一些特殊的类型来说函数体里面的代码执行的结果可能会是错的,所以为了解决这个问题就有了函数模板的特化,在特化版本的函数里面可以执行特定的操作从而获得想要的结果,那么接下来我们就要看看如何进行函数模板的特化

如何进行特化

函数模板的特化通常分为下面几个步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

比如说我们要对上面的类函数模板进行特化,就可以这么做,首先得有一个基础的函数模板:

	template<class T>
	bool less(const T& x, const T& y)
	{
		return x < y;
	}

在这个模板下面再写上一个template并加上一个尖括号,因为这里是特化的版本,所以尖括号里面可以不加上任何内容:

	template<class T>
	bool less(const T& x, const T& y)
	{
		return x < y;
	}
	template<>

因为要特化的是less函数,所以在下面就先写less函数的返回值,再写less函数的函数名,在函数名后面添加一个尖括号,在尖括号里面填入你需要特化的类型,这里想特化的int类型的参数,所以在尖括号里面填入int,最后在尖括号里面填入函数的参数列表,因为这里已经使用了特化,所以参数就可以直接写明无需再推理,那么这里的代码就如下:

	template<class T>
	bool less(const T& x, const T& y)
	{
		return x < y;
	}
	template<>
	bool less<int*>( int* const& x,int* const& y)
	{
		return *x < *y;
	}

这样函数模板的特化就完成了,再运行一下上面的测试代码便可以看到运行的结果是正确的:

在这里插入图片描述
那么这就是函数模板特化的过程希望大家能够理解。

类模板的特化

既然函数模板可以特化那么同样的道理类模板也是可以实现特化的,那这里特化的过程跟上面的函数模板的特化是一样的,首先需要一个基础类模板:

	template<class T1,class T2>
	class tem
	{
	public:
		tem()
		{
			cout << "我是一个普通类模板" << endl;
		}
	private:
		T1 _x;
		T2 _y;
	};

在这个类下面再添加一个template并跟上一个尖括号并且尖括号里面什么内容都没有,然后在下面定义特化的类,在类名后面添上一个尖括号,在尖括号里面写入你要特化的类型,比如说下面的代码:

	template<class T1,class T2>
	class tem
	{
	public:
		tem()
		{
			cout << "普通类模板特化的类型为" <<typeid(T1).name()<<" " << typeid(T2).name() << endl;
		}
	private:
		T1 _x;
		T2 _y;
	};
	template<>
	class tem<int, double>
	{
	public:
		tem()
		{
			cout << "特化的类模板特化的类型为int double" << endl;
		}
	private:
		int _x;
		double _y;
	};

这样一旦实例化的类型为int和double的话就会匹配到特化版本的类,比如说下面的代码:

int main()
{
	YCF::tem<int, int> test1;
	YCF::tem<int, double> test2;
	return 0;
}

这段代码的运行结果如下:
在这里插入图片描述
符合我们的预期那么这就是类模板的特化,跟函数模板的特化差不多。

部分特化

在对类模板的参数进行特化的时候,可以只对参数类表中的一部分参数实行特化,比如说一个模板有两个参数,我们在实现特化的时候可以只告诉编译器一个参数的类型,另外一个参数让编译器根据我们传的参数自己进行推断,比如说下面代码:

	template<class T2>
	class tem<int, T2>
	{
	public:
		tem()
		{
			cout << "部分特化的类模板特化的类型为int" << " " << typeid(T2).name() << endl;
		}
	private:
		int _x;
		T2 _y;

	};

这里我们就只特化了两个参数中的一个,所以在写template的时候得在尖括号里面加上一个class 参数名,在函数名后面的尖括号里也得加上参数名,因为另外一个参数还需要被编译器推断,那这里我们可以用下面的代码进行测试:

int main()
{
	YCF::tem<int, int> test1;
	YCF::tem<int, double> test2;
	YCF::tem<int, char> test3;
	return 0;
}

代码的运行结果如下:
在这里插入图片描述

模板特化参数的限制

在对模板的参数进行特化的时候,我们可以不告诉模板具体的参数类型,而是对模板的参数进行一定的限制,比如说一个模板有两个参数,在特化的时候就可以限制这两个参数都得为指针或者引用,比如说下面的代码:

	template<class T1,class T2>
	class tem
	{
	public:
		tem()
		{
			cout << "普通类模板特化的类型为" <<typeid(T1).name()<<" " << typeid(T2).name() << endl;
		}
	private:
		T1 _x;
		T2 _y;
	};
	template<class T1,class T2>
	class tem<T1*, T2*>
	{
	public:
		tem()
		{
				cout << "参数限制的类模板特化的类型为" << typeid(T1*).name() << " " << typeid(T2*).name() << endl;
		}
	private:
		T1 _x;
		T2 _y;
	};

在函数名后面的尖括号里面对特化的参数进行限制,在两个参数名的后面都加上*表明两个参数都是指针才能匹配这种特化类型,我们来看看下面的测试代码的运行结果:

int main()
{
	YCF::tem<int, int> test1;
	YCF::tem<int, double> test2;
	YCF::tem<int, char> test3;
	YCF::tem<int*, double*> test4;
	return 0;

这段代码的运行结果如下:
在这里插入图片描述
这里大家要注意一下,模板的部分特化只有类模板才有函数模板是不支持部分特化的,而对模板参数进行限制的特化函数模板和类模板都有。

模板的匹配规则

看到这里想必大家已经知道了类模板和函数模板是如何进行特化的?以及部分特化和参数进行限制的特化是什么意思,那这里就有个问题有这么多特化的形式,那模板又是如何进行匹配的呢?我们给的这个参数它匹配的是全特化类型还是模板参数受限制的特化类型呢?那这里我们就通过一个生活实例来带着大家了解一下这个问题,有一个小孩它的爸爸妈妈都很忙,中午一般都无法回家给那个孩子做饭吃,所以小孩中午一般都得靠自己来获取美食,那这里获取美食的方法分为三种类型:第一种就是:点外卖来获取没事这种方法是最轻松的只需要开个门就能拿到自己想吃的东西 小孩最喜欢这种方法因为非常的轻松方便,第二种就是泡泡面或者将冰箱里面的剩菜剩饭放到微波炉里面热一下这种方法也比较轻松但是相比于第一种方法还是麻烦不少小孩只有在没有钱的时候会采取这样的方法,第三种就是自己洗菜,切菜,炒菜来制作美食,这种方法就非常的麻烦也非常的累,小孩最不喜欢的就是这种方法,只有在又没有钱又没有泡面和剩菜剩饭的时候采取这样的方法,而编译器就是这样的小孩,当编译器发现有全特化的时候绝对不会去推断参数的类型,当发现全特化的类型都不符合的时候才会去看看部分特化和参数限制的特化,只有当部分特化和参数限制的特化都不符合的时候才会自己推断参数类型并生成对应实例,那么这就是模板匹配的规则,大家可以通过上面的例子理解一下。

模板的分离编译

在之前的学习种我们说函数的声明一般都放到.h结尾的头文件里面,函数的实现就放到另外一个源文件里面,这样就可以实现函数的声明与实现的分离,可以更加的有利于我们读代码和修改代码以及分享代码,比如说我们实现了一个add函数,那么在add.h的头文件里面就存放add函数的声明:

//add.h文件
int add(int x, int y);

在add.cpp文件里面 存放该函数的实现:

#include"add.h"
int add(int x, int y)
{
	return x + y;
}

那么我们只要在主文件里面包含add.h文件就可以使用add函数比如说下面的代码:

//template.cpp文件
#include"add.h"
int main()
{
	int x = 3;
	int y = 5;
	cout << "x和y相加的结果为:" << add(x ,y) << endl;
	return 0;
}

这段代码的运行结果如下:
在这里插入图片描述
上述的过程我们称为函数的分离编译,那模板函数也是一个函数,那模板函数是不是也可以分离编译呢?答案是可以的,比如说下面的代码:

//add.h文件
template<class T>
T add(T x, T y);

//add.cpp文件
#include"add.h"
template<class T>
T add(T x, T y)
{
	return x + y;
}

这里大家得注意的一点就是在add.cpp文件里面它是不认识T是什么意思的,所以得加上一个template<T>来告诉编译器这个T是模板的一个参数。说到函数声明和编译的分离,那就不得不提到类中的函数,函数在里面也可以实现声明和编译的分离,比如说我们创建一个名为calculator的类,这个类里面就有4个函数分别为:add函数(得到两个数相加的结果),sub函数(得到两个数相减的结果),mul函数(得到两个数相乘的结果),div函数(得到两个数相除的结果),这四个函数虽然在类里面但是依然可以实现分离编译比如说下面的代码:

//calculator.h文件
class calculator
{
public:
	calculator(const int& _x, const int& _y);
	int add();
	int sub();
	int mul();
	int div();
private:
	int x;
	int y;
};

//calculator.cpp文件
#include"calculator.h"
int calculator::add()
{
	return x + y;
} 
int calculator::div()
{
	return x / y;
}
int calculator::mul()
{
	return x * y;
}
int calculator::sub()
{
	return x - y;
}
calculator::calculator(const int& _x, const int& _y)
	:x(_x)
	, y(_y)
{}

这里在实现分离编译的时候得告诉编译器这个函数是属于哪一个作用域的,所以在每个函数名的前面都得加上指定的作用域,那么我们就可以用下面的代码进行测试:

#include"calculator.h"
int main()
{
	calculator tem(20, 20);
	cout << "相加的值为:" << tem.add() << endl;
	cout << "相乘的值为:" << tem.mul() << endl;
	cout << "相减的值为:" << tem.sub() << endl;
	cout << "相除的值为:" << tem.div() << endl;
	return 0;
}

代码的运行结果如下:
在这里插入图片描述
既然类里面的函数可以实现分离编译,那模板类里面的函数也可以实现分离编译吗?答案是可以的,但是你得在每个函数的上面都加上一个template<模板参数>来告诉编译器里面的一些名字是模板的参数,并且函数名前面的作用域名不能是类的名字而得是类型的名字,因为模板的存在类名加上模板的参数才能变成类型名,那么模板类的分离定义的代码就如下:

//calculator.h文件
template<typename T>
class calculator
{
public:
	calculator(const T& _x, const T& _y);
	T add();
	T sub();
	T mul();
	T div();
private:
	T x;
	T y;
};

//calculator.cpp文件
#include"calculator.h"
template<class T>
T calculator<T>::add()
{
	return x + y;
}
template<class T>
T calculator<T>::div()
{
	return x / y;
}
template<class T>
T calculator<T>::mul()
{
	return x * y;
}
template<class T>
T calculator<T>::sub()
{
	return x - y;
}
template<class T>
calculator<T>::calculator(const T& _x, const T& _y)
	:x(_x)
	, y(_y)
{}

但是我们将上面的代码运行一下就会发现这里好像编译不过去,并且编译器报出了这样的错误:

在这里插入图片描述
这是为什么呢?我们来看看另外一个报错的信息:
在这里插入图片描述
这个跟我们说出现错误的类型为链接错误,什么是链接错误,链接错误就是找不到这个东西的定义就会爆出链接错误,而这里爆出链接错误的地方为:构造函数,add函数,mul函数,sub函数,div函数,也就是说我们实现声明和定义分离的函数都出现了链接错误,也就是说这些函数找不到自己的定义在哪里,那这是为什么呢?要想知道这个问题我们就得从程序翻译的过程来理解,首先一个程序在生成可执行程序的过程中会经历这么几个阶段:首先是预处理生成.i文件 ,然后再是通过编译生成.s文件,然后再通过汇编生成.o文件,最后通过链接生成可执行程序,在预处理的时候编译器会将每个文件包含的头文件进行展开,在我们写的程序里面有两个文件包含了头文件:
在这里插入图片描述
在这里插入图片描述
那么在预处理的时候就会将头文件里面的内容复制到包含头文件的地方,那这样的话calculator.cpp的内容就如下:

template<typename T>
class calculator
{
public:
	calculator(const T& _x, const T& _y);
	T add();
	T sub();
	T mul();
	T div();
private:
	T x;
	T y;
};
template<class T>
T calculator<T>::add(){return x + y;}
template<class T>
T calculator<T>::div(){return x / y;}
template<class T>
T calculator<T>::mul(){return x * y;}
template<class T>
T calculator<T>::sub(){return x - y;}
template<class T>
calculator<T>::calculator(const T& _x, const T& _y):x(_x), y(_y){}

template.cpp文件里面的内容就变成下面这样:

template<typename T>
class calculator
{
public:
	calculator(const T& _x, const T& _y);
	T add();
	T sub();
	T mul();
	T div();
private:
	T x;
	T y;
};
int main()
{
	calculator<int> tem(20, 20);
	cout << "相加的值为:" << tem.add() << endl;
	cout << "相乘的值为:" << tem.mul() << endl;
	cout << "相减的值为:" << tem.sub() << endl;
	cout << "相除的值为:" << tem.div() << endl;
	return 0;
}

因为我们在main函数里面调用模板类里面的函数,所以在编译的过程中程序会call函数的地址来调用函数里面的内容,可是这里存在一个问题,在编译阶段template.cpp文件里面存在函数的地址吗?是没有的因为这里只有函数的声明没有函数的实现,所以这里编译器得找函数的地址,那编译器去哪里找函数的地址呢?答案是在链接的时候通过各个.o文件的符号表里面去找,所以这里就会去calculator.o的符号表里面去找,那这里爆出错误说函数的定义找不到,本质上就是编译器找不到函数的地址,也就是在calculator.o的符号表里面找不到函数的地址,那这是为什么呢?明明我们定义了函数为什么会找不到链接不上呢?原因很简单,在链接之前各个文件之间的操作都是单线操作,在链接的时候会融合到一起:
在这里插入图片描述
我们说符号表里面会装着函数,全局变量等一系列东西的地址,可是这里有个问题我们在main函数里面调用函数的时候调用的是模板吗?好像不是的吧,我们调用add函数调用mul函数等等调用的都是具体的对象,可是模板是对象吗?好像不是的吧,只有我们调用模板告诉模板要实例化的类型和数据什么的之后,模板才会实例化出来对象,而我们调用的也是这个实例化之后的东西,而生成符号表之前都是单线操作,我们在calculator.cpp文件里面有实例化操作吗?是没有的,所以模板就不会实例化出来对象,所以calculator.cpp文件里面也就不会存在对象或者函数的地址,所以链接在找函数的地址的时候就会找不到,声明的地方不会存在地址,有地址的地方没有实例化出来,所以就报出了链接问题,那么要想解决这个问题就有两个方法,第一种就是显示实例化:在calculate.cpp文件里面加上一个template,然后在template的下面加上你要实例化的类型比如说你要实例化ine类型的calculator就在template的下面加上class calculator<int>;这里不用加上类型名,那么完整的代码就如下:

//calculator.cpp文件
#include"calculator.h"
template<class T>
T calculator<T>::add(){return x + y;}
template<class T>
T calculator<T>::div(){return x / y;}
template<class T>
T calculator<T>::mul(){return x * y;}
template<class T>
T calculator<T>::sub(){return x - y;}
template<class T>
calculator<T>::calculator(const T& _x, const T& _y)
	:x(_x)
	, y(_y)
{}

calculator<int> tem;//显示实例化

再将上面的代码运行一下就可以发现可以正常地运行了:
在这里插入图片描述
那这种实现地方法是正确的吗?我们再在main函数里面实例化一个double类型的对象:

#include"calculator.h"
template<typename T>
int main()
{
	calculator<int> tem(20, 20);
	cout << "相加的值为:" << tem.add() << endl;
	cout << "相乘的值为:" << tem.mul() << endl;
	cout << "相减的值为:" << tem.sub() << endl;
	cout << "相除的值为:" << tem.div() << endl;
	calculator<double> tem1(20.0, 20.0);
	cout << "相加的值为:" << tem1.add() << endl;
	cout << "相乘的值为:" << tem1.mul() << endl;
	cout << "相减的值为:" << tem1.sub() << endl;
	cout << "相除的值为:" << tem1.div() << endl;
	return 0;
}

然后运行一下代码就会发现又报错了:
在这里插入图片描述
这里的报错也是链接的错误,跟之前的错误一摸一样,原因就是我们在calculator.cpp文件里面显示实例化了int类型的函数和类,但是没有实例化double类型的函数和类,所以这里就报了链接错误,那么只要我们在calculator.cpp文件里面再实例化一个double类型就可以解决上面的问题,但是这样的解决方法显然就很麻烦,我们会遇到各种各样的数据类型,如果我们每个类型都要自己手动的去显示实例化一份的话就很麻烦,所以我们采用第二种方法来解决上述的问题就是不讲函数的声明和定义进行分离,将.cpp文件里面的内容全部放到.h文件里面就可以了,那这里.h文件里面的内容就如下:

//calculator.h文件
template<typename T>
class calculator
{
public:
	calculator(const T& _x, const T& _y);
	T add();
	T sub();
	T mul();
	T div();
private:
	T x;
	T y;
};
template<class T>
T calculator<T>::add() { return x + y; }
template<class T>
T calculator<T>::div() { return x / y; }
template<class T>
T calculator<T>::mul() { return x * y; }
template<class T>
T calculator<T>::sub() { return x - y; }
template<class T>
calculator<T>::calculator(const T& _x, const T& _y)
	:x(_x)
	, y(_y)
{}

再运行一下上面的测试代码就可以发现即使不显示实例化也能正常的运行:
在这里插入图片描述那这是为什么呢?原因很简单我们在.h里面添加了函数的定义,template.cpp文件在预处理的时候i就将.h文件里面的内容展开了,所以在这个文件里面既有函数的声明又有函数的定义所以文件在链接的时候根本就不用去其他文件的符号表里面找函数的地址,本文件里面就有,所以就不会报链接的错误,那么这就是第二种解决方法,希望大家能够理解。

模板的总结

【优点】

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

【缺陷】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长

  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

叶超凡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值