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

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

时间:2024-08-16 20:23:25浏览次数:19  
标签:初阶 进阶 示例 实例 Date array 特化 模板 函数

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


睡觉待开机:个人主页

个人专栏: 《优选算法》《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

标签:初阶,进阶,示例,实例,Date,array,特化,模板,函数
From: https://blog.csdn.net/2302_79031646/article/details/141259414

相关文章

  • python入门篇-day05-函数进阶
    函数返回多个值不管返回多少都会默认封装成一个元组的形式返回(可以自定义返回的数据类型,如:[]{})思考:1个函数可以同时返回多个结果(返回值)?答案:错误,因为1个函数只能返回1个结果,如果同时返回多个值则会封装成1个元素进行返回.演示#需求:定义函数my_cal......
  • 合宙Air780EP模组LuatOS脚本开发MQTT应用示例
    本文详细讲解了基于合宙Air780EP模组LuatOS开发的多个MQTT应用示例。本文同样适用于合宙的以下型号:Air780EPA/Air780EPT/Air780EPSAir780E/Air780EX/Air201…一、相关准备工作1.1硬件准备合宙EVB_Air780EP开发板一套,包括天线、SIM卡;USB线PC电脑1.2软件准备登录合宙......
  • C# WPF现代化开发:绑定、模板与动画进阶
    ......
  • 【JAVA】深入理解守护线程与非守护线程:概念、应用及示例
    文章目录介绍1.线程的基础知识2.守护线程与非守护线程2.1什么是守护线程?特点:2.2什么是非守护线程?特点:3.为什么需要守护线程?示例:后台任务处理示例:日志记录4.非守护线程的应用场景示例:数据库连接处理5.守护线程与非守护线程的对比6.总结更多相关内容可查......
  • DuckDB_SQL-使用示例以及和PG之间的概念
    duckdbCatalog(目录):表示整个数据库或数据库管理系统。一个数据库服务器可以包含多个数据库,每个数据库都有自己的Catalog1.database--catalogcatalog_namedatabase:In‑Memoryvs.PersistentDatabasedatabase--database_listnew_db.my_schema:system......
  • IMU惯性测量模块在ROS环境下的应用示例
    Ubuntu版本:20.04;ROS环境:noetic;IMU型号:亚博10轴IMU惯导模块目录一.ROS环境配置1、在终端运行对应的命令 2、安装ROS串口驱动二、IMU软件包使用1、新建、编译工作空间 2、绑定IMU端口3、修改参数配置 三、运行可视化界面 1、运行launch文件2、可能遇到的问题3、......
  • Linux打包命令tar极简示例_2
    只解压tar包中的某个文件这是tar包:只解压a.txt:上边的例子不大理想,再来一个tar包里带目录的:再弄个gzip压缩过的吧:......
  • Datawhale AI 夏令营-天池Better Synth多模态大模型数据合成挑战赛-task2探索与进阶(
    在大数据、大模型时代,随着大模型发展,互联网数据渐尽且需大量处理标注,为新模型训练高效合成优质数据成为新兴问题。“天池BetterSynth-多模态大模型数据合成挑战赛”应运而生,旨在探究合成数据对多模态大模型训练的影响及高效合成方法策略,推动多模态大模型数据合成创新。比赛关......
  • 前端进阶——浏览器篇
     浏览器如何工作(一)进程架构 浏览器的工作过程复杂而高效,其核心在于其进程架构的设计。以下是对浏览器进程架构的详细解析:一、浏览器的主要进程现代浏览器大多采用多进程多线程的架构,以Chrome浏览器为例,其主要进程包括:浏览器进程(BrowserProcess):功能:作为浏览器的主进......
  • SQL进阶技巧:数据清洗如何利用组内最近不为空的数据填充缺失值。【埋点日志事件缺失值
    目录0引言1问题描述2数据准备 3问题分析4小结0引言  在用户行为分析中,我们往往需要对用户浏览行为进行分析或获客的渠道进行分析,在埋点日志中用户一个session中会浏览不同的界面,会进行url的跳转,在前端埋点时,往往将用户刚进入界面时的url进行存储,后续在当前......