【c++】深入学习c++中的模板

文章目录


前言

我们在前几篇的学习中只是使用了最简单的模板,比如用一个模板类等等,我们这篇的学习将在以前的基础上再深入的学习一下模板。


一、非类型模板参数

模板参数分类类型形参与非类型形参
类型形参即:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称
非类型形参,就是用一个常量作为类 ( 函数 ) 模板的一个参数,在类 ( 函数 ) 模板中可将该参数当成常量来使用。
下面我们来演示一下:
#define N 10
//类型模板参数
template<class T>
class array
{
public:

private:
	T _a[N];
};

int main()
{
	Array<int> a1;
	Array<double> a2;
	return 0;
}

在以前,我们想要在一个类中定义一个模板数组能实现存放不同个数的数组是不行的,因为我们define的N是一个固定值,如下图:

7b708cab6b614659afb42ab9c8cac945.png

 我们如何让两个数组存放的类型不同存放的个数也不同呢?这个时候非类型模板参数的效果就来了,如下:

//非类型模板参数
template<class T,size_t n = 10>
class Array
{
public:

private:
	T _a[n];
};

int main()
{
	Array<int,5> a1;
	Array<double,8> a2;
	return 0;
}

b4b9d2d677ba49d38e5dd6a694d6496d.png

 我们可以看到这两个数组类型不同,数组元素个数也不同。在这里要记住,非类型模板参数一定是整形常量,不可以是浮点数,类对象以及字符串。

7023dd15993b4022acdb04cff2e2c5e5.png

扫描二维码关注公众号,回复: 14607152 查看本文章

15640c9a378245149f51ff4152201e90.png 讲到这里我们可以对比一下c++11的新特性的array.

e29bcd0c69cf4fe9ad1ebebe9af6c727.png

这个数组和传统数组的区别在于,传统数组对于越界检查是抽查,array对于越界检查是全面检查。

9e7ba9e08e444640aa0a425fe0c20add.png 8bf6234c2e67474c81bbb7abc47b7a97.png

从上图我们可以看到,传统数组都越界写了竟然没有检查出来越界,而array是可以检查出来的。但是这个array我们会用吗?其实不会,因为我们直接用vector是最方便的,没必要用array,并且array的空间在栈上,如果数据过多会将栈搞崩溃,而vector空间在堆上完全不受影响。 

二、模板的特化

1.引入概念

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};
template <class T>
bool Less(T left,T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl;  //可以比较,结果错误
	return 0;
}

对于上面的代码我们用了以前的日期类做例子,然后用一个Less函数比较日期的大小,通过结果发现对于指针的比较是错误的,如下图:

ddb68f70787048418323e77d9a130933.png

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。

 那么这个时候该怎么办呢?我们就需要对模板进行特化,只针对Date*类型,如下图:

//模板特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

 对于模板特化我们必须在函数名后面显式类型,看一下特化后的结果:

649efa9629e946d891753ab25e5b541f.png

 可见确实完成了我们的任务,刚刚我们演示的模板特化被称为全特化,就是参数的类型都是确定的。

//模板特化   半特化
template<class T>
bool Less(T* left, T* right)
{
	return *left < *right;
}

a0cce2eb741d4575a9ca2bf0253ebc29.png 我们的特化不一定非要确认类型,比如上面的半特化直接将所有指针类型包含了,我们的两个int*的指针去比较结果也是正确的。

模板特化分为函数模板特化和类模板特化。

下面我们讲解一下类模板特化:

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
template<>
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>" << endl; }
private:
	int _d1;
	char _d2;
};

int main()
{
	Date<int,int> d1;
	Date<int, char> d2;
	return 0;
}

对于类模板特化我们可以特化不同版本的类,比如上面代码中的(int,int)的类和(int,char)的类。并且上述代码中都是全特化,将模板参数列表中所有的参数都确定化,下面我们在演示一下偏特化:

template<class T1, class T2>
class Data
{
public:
 Data() {cout<<"Data<T1, T2>" <<endl;}
private:
 T1 _d1;
 T2 _d2;
};

偏特化就是对参数的进一步限制。我们演示一下偏特化的部分特化:

// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
 Data() {cout<<"Data<T1, int>" <<endl;}
private:
 T1 _d1;
 int _d2;
};

 当然我们的模板参数也可以是引用:

//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
 Data(const T1& d1, const T2& d2)
 : _d1(d1)
 , _d2(d2)
 {
 cout<<"Data<T1&, T2&>" <<endl;
 }
 
private:
 const T1 & _d1;
 const T2 & _d2; 
 };

 引用做模板参数不是很常见,但是我们在实现list的迭代器的时候见到过模板参数是引用。

2.模板的分离编译

什么是模板分离编译?一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

模板是不支持分离编译(在两个文件)的,会引发奇怪的报错。 如下图:

05dbc2cdaa1a46d48c87ace5892f7e42.png

6fde151579334a41b3239f2b6e1104c0.png febc93146f274f04b5322ceae06ecce2.png

 再说为什么出现这种情况之前我们先讲一下c/c++程序运行一般要经过的几个步骤:

fae0f635835546418f979a5bd6617a5d.png

 我们发现同样的方式我们定义一个函数就能正常运行,如下图:

18486274c382436486189f806cc3e62c.png

c6872879fa0c42f9a4d1187f9a7724b5.png

77cc02cbe5c546aab003c4ec6cb0292f.png526b90f70afc4ab297e128c16756025d.png

 现在我们就通过汇编分析一下为什么会这样:

f692e91e180c4b3d92ecda703a9f5dbc.png

 我们发现函数func会跳到0611348h这个函数地址,在这里func会被编译成一堆指令,所以在func.o中有函数的地址,而模板是没有函数的地址的,因为模板需要实例化后才有具体的地址,刚开始地址为空如下图:

bded0ce4dcd647b9a559d63deb315dbe.png

在编译的时候在地址处打个问号,等到链接的时候再去拿Add的地址,如下图:

15b7bfe20640407cb7951821988ceba3.png

等到链接的时候找到func的地址,但是却找不到add的地址,如下图:

3050b82650a946adba35d45d5fa6647e.png 也就是说模板分离编译出错的原因是链接的时候无法拿到模板函数的地址,那么有办法可以解决吗?其实是有的,但是不常用因为每个类型都需要显示实例化:

e321a65f20014788b1fd3267c1df5682.png

069c32c61e444cd5994d1aab39cdf3f7.png

 这个时候我们把int类型的add放出来又不行了:

c5de4e731c9f48fbaf35bef9fe0dfd92.png

 要想解决还是刚刚显示实例化的方法,但是这样就会很繁琐。所以我们最好的办法就是不要分离在两个文件,就在一个文件中实现。如下图:

4f25bbd7c0de4d34b4a31c897be712a6.png

2076edfed81841a2995671a07326c7c3.png以上就是本篇文章的所有内容,最重要的是理解为什么模板分离编译会报错并且如何解决这个问题

注意:

对于模板的报错,我们一定要先解决报错的第一个,因为通常模板是有一个问题带出来的其他好多问题,将第一个问题解决了就解决了其他问题。


总结

【优点】
1. 模板复用了代码,节省资源,更快的迭代开发, C++ 的标准模板库 (STL) 因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/129887581
今日推荐