【CPP】C++模板:初阶到进阶语法与实用编程示例

关于我:在这里插入图片描述


睡觉待开机:个人主页

个人专栏: 《优选算法》《C语言》《CPP》
生活的理想,就是为了理想的生活!
作者留言

PDF版免费提供倘若有需要,想拿我写的博客进行学习和交流,可以私信我将免费提供PDF版。
留下你的建议倘若你发现本文中的内容和配图有任何错误或改进建议,请直接评论或者私信。
倡导提问与交流关于本文任何不明之处,请及时评论和私信,看到即回复。



1.前言

引言:模板编程的精髓
C++模板是泛型编程的基石,它们为开发者提供了编写类型安全、性能高效且高度可重用代码的能力。本文将深入探讨模板的初阶、进阶语法,揭示其强大功能。

深入进阶语法特性
我们将一起探索模板的高级语法,包括特化、偏特化,并通过实际的编程示例,展示这些特性如何解决复杂问题,提升开发效率。

实践与提升
通过本文的学习,您将获得优化模板性能的策略,并学习如何遵循最佳实践来编写清晰、高效的代码。无论您是初学者还是希望深化理解的资深开发者,本文都旨在助力您的C++模板编程技能提升。


在介绍模的进阶语法之前,我们先来简单提及一下初阶模板的相关语法,来与进阶模板语法部分有一个更好的衔接和过渡。
因为模板初阶的内容比较少,相关的示例代码也不太多,所以我干脆把整个模板初阶做成了一个大标题,这样就可以快速梳理一下关键的初阶模板语法了。

2.初阶模板

2.1模板的基础概念

模板:与类型无关的通用代码,用于代码的复用。

在分类上,我们可以简单分为函数模板类模板

  • 函数模板:显而易见,用于生成函数的模板,我们称之为函数模板
  • 类模板:用于生成类的模板,我们称之为类模板

下面来依次介绍两种模板,即函数模板类模板

2.2初识函数模板

函数模板的语法格式是这样的(见下面举例):
在这里插入图片描述

2.3函数模板的实例化(重点)

什么是实例化呢?模板对于编译器来说,是没有实际意义的,因为模板是不可被直接编译的,因而首先要根据我们给到模板的类型,编译器帮助我们生成一份具体的函数/类,这样编译器才可以进行编译,运行代码。
在这里插入图片描述

其实,根据我们写法的不同,编译器有两种由模板生成对应具体函数的模式。
一种是跟上面意义,我们传给他参数调用Swap函数的时候没有写具体的类型给Swap,这样编译器会自动根据传入生成对应函数的类型,我们称之为“隐式类型转换”。

与之对应的,还一种叫做“显示类型实例化”的方式,比如下面的举例。
在这里插入图片描述
在上面图文中,我们简单介绍了一下“隐式实例化”与“显式实例化”,但这仅仅是建立在只有一个模板的情况下,如果这时候有个与模板同名的函数,我们调用函数他会通过模板实例化还是直接调用函数呢?
为了回答上面问题,我们来谈谈模板参数的匹配原则。

2.4模板参数的匹配原则

当我们写出一个函数调用来的时候,这个函数调用是匹配模板生成的实例化函数呢还是我们写的那个重名函数呢?

对于模板的匹配原则,我想我可以给出三点规律:

  • 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
    在这里插入图片描述
    在这里插入图片描述
    意思就是说,程序员想用哪个用哪个(前提是两个都满足的话)。

  • 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
    在这里插入图片描述
    在这里插入图片描述

  • 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
    在这里插入图片描述
    在这里插入图片描述

2.5类模板初识

在这里插入图片描述
在这里插入图片描述

2.6类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

在这里插入图片描述

2.7模板的注意事项(重点)

  1. 模板的类名和类型可能会不一致。比如说class A类,类名是A,但是我们在定义的时候需要写为A a,这时候类型是A
  2. 模板类,声明与定义要尽量放在同一个文件中。因为如果要分离的话,需要给函数定义部分也要加上模板的声明才能编过。并且强烈不建议这样做。在这里插入图片描述3. 类模板声明与定义分离不能在两个文件中。也不是完全不能,原因见模板进阶。

3.非类型模板参数

模板的参数上面我们只说到了一种,都是类型模板参数,其实还有一种模板参数比较特殊:非类型模板参数,也就是“常数参数”。
在这里插入图片描述

本质:把常量值作为参数传入模板,编译器生成不同的类,在C99只支持整形,C11以后支持其他类型

为什么有非类型模板参数(常数参数),有什么意义呢?我在类/函数里面直接写一个常数不是更加方便吗?
我们写的所有常量值赋给一个变量,都是在编译之后才会实现这个效果的,但是模板实例化的时候如果涉及到开固定空间的话因为还没有编译所以就会造成模板没办法继续实例化的情况,所以引入了特殊的非类型模板参数——常数参数

那有什么经典案例吗?——array

4.非类型模板参数经典案例——array

STL在CPP11的时候引入了一个新的容器——array,也就是我们C语言常用的数组升级版,该容器是一个固定大小的数组,该容器的实现就是一个模板,里面用到了非类型模板参数。
我们知道,数组在编译之前要确定好大小的,但是这个确定大小的常数是在模板调用里面的,具体array也不知道要初始化多大空间。所以就用到了非类型模板参数——常数参数。

既然说到了array,我们来简单说一下他相对于C语言中的数组来说有哪些优势吧。
array:相比于之前的数组,越界检查更加严格,直接用的断言检查。

然而,相对于vector,显得有些多余。
在这里插入图片描述
相比于vector,顺序表,array没什么优势。主要体现在下面几点:

4.1初始化方面

array官方没有提供初始化接口,而vector则有初始化接口。

array<int,10> a;//这里官方没有提供初始化构造函数哈,不是我刻意不用
vector<int> v(10, 1);

cout << "array:" << endl;
for (auto& e : a)
{
	cout << e << " ";
}//-858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460 -858993460
cout << endl;

cout << "vector:" << endl;
for (auto& e : v)
{
	cout << e << " ";
}//1 1 1 1 1 1 1 1 1 1
cout << endl;

4.2栈溢出方面

array很容易造成栈溢出问题,因为array是建立在栈上空间的一种容器(为了模仿数组)。而vector是直接把空间放到堆上的,堆的空间远大于栈,所以说array更容易造成栈溢出问题,vector造成的顶多是堆溢出…

//array<int, 1000000> a;//代码为 -1073741571
vector<int> v(1000000, 1);//代码为 0

4.3越界检查

在越界检查这一方面,vector与array都是采用了断言的方式进行越界检查。

array<int, 10> a;
vector<int> v(10, 1);

for (int i = 0; i < 11; i++)
{
	a[i] = i;
}
//断言报错

for (int i = 0; i < 11; i++)
{
	v[i] = i;
}
//断言报错

两者都是断言报错。

我们总结一下,vector与array的区别:
在这里插入图片描述

5.模板的按需实例化特性

模板实例化:编译器不会直接编译模板,而是先根据你给的类型生成对应的实例化类,在对实例化后的类进行编译。
模板的按需实例化:编译器对于要实例化的类模板,不会直接全部实例化,编译器只会挑出用到的进行实例化。

比如,显然,我们发现下图中size调用是错误的,但是如果我们不去调用这个operator[]编译器是不会报错的,因为压根就没实例化。
在这里插入图片描述

//szg::array<int, 100> a;//代码为 0
szg::array<int, 100> a;
a[0] = 0;//直接语法报错

6.模板特化

模板特化:在模板实例化的时候针对某种类进行特殊处理。
在这里插入图片描述

6.1函数模板特化支持对类模板进行特殊类型处理

class Date
{
public:
	friend ostream& operator<<(ostream& _cout, const Date& d);

	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);
	}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

template<class T>//函数模板
bool Less(T left, T right)
{
	return left < right;
}
//bool Less(Date* left, Date* right)//函数重载也能表现为特化,不过本质是一种重载
//{
//	return *left < *right;
//}
template<>//函数模板特化
bool Less(Date* left, Date* right)
{
	return *left < *right;
}

void test5()
{
	//一般类型的比较
	cout << Less(1, 2) << endl;//1
	//自定义类型的比较
	cout << Less(Date(1, 1, 1), Date(2, 2, 2)) << endl;//1
	//自定义类型指针的比较
	cout << Less(new Date(1, 1, 1), new Date(2, 2, 2)) << endl;//1,这个地方走的是特化

}

注意:对于函数模板的特定类型特化,也可以使用函数重载进行特化,虽然本质上属于重载,但是效果与特化一样。

6.2函数模板特化也支持模板写法

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

void test5()
{
	//一般类型的比较
	cout << Less(1, 2) << endl;//1
	//自定义类型的比较
	cout << Less(Date(1, 1, 1), Date(2, 2, 2)) << endl;//1
	//自定义类型指针的比较
	cout << Less(new Date(1, 1, 1), new Date(2, 2, 2)) << endl;//1,这个地方走的是特化

}

实际上,我感觉这个模板+特化就是写了一个更合适的模板匹配而已。

建议使用重载的方法进行特化,因为比较复杂。

6.3类模板特化

特化分为全特化半特化,下面来进行举例说明。
请注意,这里所说的半特化意思是对类型有限制,使类型特化局限于某一类,这是半特化。

//类模板
template<class T1, class T2>
class A
{
private:
	T1 _a;
	T2 _b;
public:
	A()
	{
		cout << "template<class T1, class T2>" << endl;
	}
};
//全特化类模板
template<>
class A<double, double>
{
private:
	double _a;
	double _b;
public:
	A()
	{
		cout << "class A<double, double>" << endl;
	}
};
//类模板的半特化
template<class T1>
class A<T1, int>
{
private:
	T1 _a;
	int _b;
public:
	A()
	{
		cout << "class A<T1, int>" << endl;
	}
};
//类模板的半特化,不一定是特化一半参数,这里代表对类型进行限制都称为半特化。
template<class T1, class T2>
class A<T1*, T2*>
{
private:
	T1 _a;
	int _b;
public:
	A()
	{
		cout << "class A<T1*, T2*>" << endl;
	}
};

void test6()
{
	A<char, char> a1;//函数模板template<class T1, class T2>
	A<double, double> a2;//全特化class A<double, double>
	A<double, int> a3;//半特化class A<T1, int>
	A<double*, int*> a4;//半特化class A<T1*, T2*>
}

6.4类模板的特化应用

priority_queue<Date, vector<Date>, greater<Date>> pq;

Date d1(2024, 4, 8);
pq.push(d1);
pq.push(Date(2024, 4, 10));
pq.push({ 2024, 2, 15 });

while (!pq.empty())
{
	cout << pq.top() << " ";
	pq.pop();
}
cout << endl;

priority_queue<Date*, vector<Date*>> pqptr;
pqptr.push(new Date(2024, 4, 14));
pqptr.push(new Date(2024, 4, 11));
pqptr.push(new Date(2024, 4, 15));

while (!pqptr.empty())
{
	cout << *(pqptr.top()) << " ";
	pqptr.pop();
}//结果随机,主要是因为比较的是new出来的地址。
cout << endl;

为了解决上面问题,我们可以

  • 写一个仿函数
class GreaterPDate
{
public:
	bool operator()(const Date* p1, const Date* p2)
	{
		return *p1 > *p2;
	}
};

void test_priority_queue2()
{
	priority_queue<Date, vector<Date>, greater<Date>> pq;

	Date d1(2024, 4, 8);
	pq.push(d1);
	pq.push(Date(2024, 4, 10));
	pq.push({ 2024, 2, 15 });

	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;

	priority_queue<Date*, vector<Date*>, GreaterPDate<Date>> pqptr;
	pqptr.push(new Date(2024, 4, 14));
	pqptr.push(new Date(2024, 4, 11));
	pqptr.push(new Date(2024, 4, 15));

	while (!pqptr.empty())
	{
		cout << *(pqptr.top()) << " ";
		pqptr.pop();
	}//加上仿函数之后,结果是唯一的。
	cout << endl;
}
  • 模板特化处理
template<class T>
class GreaterPDate<T*>//右边这个T*就是进行匹配的,传的是指针的时候就会进行匹配该特化模板类
{
public:
	bool operator()(const T* p1, const T* p2)
	{
		return *p1 > *p2;
	}
};

void test_priority_queue2()
{
	priority_queue<Date, vector<Date>, greater<Date>> pq;

	Date d1(2024, 4, 8);
	pq.push(d1);
	pq.push(Date(2024, 4, 10));
	pq.push({ 2024, 2, 15 });

	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;

	priority_queue<Date*, vector<Date*>, GreaterPDate<Date>> pqptr;
	pqptr.push(new Date(2024, 4, 14));
	pqptr.push(new Date(2024, 4, 11));
	pqptr.push(new Date(2024, 4, 15));

	while (!pqptr.empty())
	{
		cout << *(pqptr.top()) << " ";
		pqptr.pop();
	}//加上仿函数之后,结果是唯一的。
	cout << endl;
}

7.模板文件分离

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

然而,由于模板按需实例化特点,这种分离编译模式可能会产生意想不到的连接错误

7.1模板分离连接错误

下面展示经典分离错误

#include"array.h"
void test7()
{
	bit::array<int, 10> a;
	cout << a.size() << endl;//无法解析的外部符号 "public: unsigned __int64 __cdecl bit::array<int,10>::size(void)const "
}
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;

namespace bit
{
	// 只支持整形做非类型模板参数
	// 非类型模板参数  类型 常量
	// 类型模板参数   class 类型
	template<class T, size_t N = 10>
	class array
	{
	public:
		size_t size() const; //声明,是模板

	private:
		T _array[N];
		size_t _size = 0;
	};

	void func();//声明,但是不是模板
}

#include"array.h"

void bit::func()
{
	cout << "func()" << endl;
}

namespace bit
{ 
	template<class T, size_t N>
	size_t array<T, N>::size() const
	{
		return 1;
	}
}//第一种写法
//template<class T, size_t N>
//size_t bit::array<T, N>::size() const
//{
//	return 1;
//}//第二种写法

在这里插入图片描述
作为对比,我们array.h里还实现了一个func函数进行分离。

#include"array.h"
void test7()
{
	bit::array<int, 10> a;
	//cout << a.size() << endl;//无法解析的外部符号 "public: unsigned __int64 __cdecl bit::array<int,10>::size(void)const "
	bit::func();//func()
}

我们发现能够正常跑,这是为什么呢?
定义的地方,不知道实例化T成什么类型,所以有定义无法实例化,也就是无法生成函数的地址到符号表
调用的地方,知道T需要实例化成什么类型,但是因为编译阶段.cpp分离,没办法把T类型给到array.cpp从而造成编译器没办法为size()实例化出来。

如何解决?

7.2解决方法

如果执意把模板声明与定义分离,那么你可以在.h文件显示实例化,告诉编译器你定义的函数模板要实例化成什么类型。

7.2.1分离,显示实例化

在这里插入图片描述

#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;

namespace bit
{
	// 只支持整形做非类型模板参数
	// 非类型模板参数  类型 常量
	// 类型模板参数   class 类型
	template<class T, size_t N = 10>
	class array
	{
	public:
		size_t size() const; //声明,是模板

	private:
		T _array[N];
		size_t _size = 0;
	};

	void func();//声明,但是不是模板

	//显示实例化
	template
	class array<int>;
	template
	class array<double>;
}
#include"array.h"
void test7()
{
	bit::array<int, 10> a;
	cout << a.size() << endl;//1
	bit::func();//func()
}

缺点:必须挨个手动显示实例化…比较麻烦

7.2.2声明与定义不分离,放在同一个.h文件中

在这里插入图片描述

建议:强烈建议对于模板声明与定义要放在同一个文件中,省得麻烦。

8.模板的优缺点

在这里插入图片描述

9.模板的意义

模板带领语言发展走上了一条快车道,开启了泛型编程的新时代。
在这里插入图片描述



好的,如果本篇文章对你有帮助,不妨点个赞~谢谢。
在这里插入图片描述


EOF

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值