C++模板初了解
这节我们来学习一下C++的一个便捷之处——模板
文章目录
一、泛型编程
泛型编程的基本思想
泛型编程的优点
泛型编程的应用
二、模板
函数模板
函数模板格式
函数模板的原理
函数模板的实例化
1.隐式实例化
2.显示实例化
函数模板的匹配原则
类模板
类模板的格式
类模板的实例化
三、STL
STL的组成
前言
在我们之前学习C语言的时候,我们初次接触编程,无论什么都得我们自己手搓,这是因为C语言是一种入门的基础编程语言,很多东西无法直接调用出来,需要我们自己手动去实现,尽管手搓代码能让我们熟悉编程,提高代码能力,但是什么东西都得自己来敲,实在有点头疼,C++作为C语言的进阶版,它明显知道了这个问题,于是他为了提高效率,做了很多东西让我们直接调用,这节我们来学习一下C++中的模板。
一、泛型编程
在学习模板之前,我们先来了解一下什么是泛型编程。泛型编程(Generic Programming)是一种编程范式,它强调编写能够处理不同数据类型的通用代码,而不依赖于特定的类型。其核心思想是通过抽象的方式让程序可以在编译时确定具体类型,而在运行时不依赖于具体类型,从而实现代码的高复用性和灵活性。
泛型编程的基本思想
-
代码重用:通过泛型编程,我们可以编写通用的算法和数据结构,这些算法和数据结构不依赖于具体的类型,而是通过参数化类型来实现。这样,我们只需编写一次代码,而它可以处理多种不同类型的数据。
-
类型独立性:泛型编程的目标是编写不依赖于特定数据类型的程序。通过模板,程序能够在编译时根据具体的类型自动生成所需的代码,而无需为每种类型编写重复的代码。
-
提高抽象层次:泛型编程鼓励编写更加抽象的代码,通常将数据的操作通过接口或者模板来进行参数化,从而使得代码更加灵活且具有较好的扩展性。
泛型编程的优点
- 提高代码复用性:通过模板,算法和数据结构可以与多种数据类型配合使用,避免了重复实现相似功能的代码。
- 减少错误:因为代码不依赖于具体类型,所以可以减少因为修改某个类型时引入的错误。
- 提高性能:模板通常是在编译时进行实例化的,这意味着通过模板创建的代码具有与手写的特定类型代码相同的性能。
- 增强可读性和可维护性:代码更加简洁和抽象,易于理解和修改。
泛型编程的应用
-
算法的通用性:很多标准算法,如排序、查找等,都可以通过泛型编程来实现,从而让它们适应不同的数据类型。例如,C++ STL(标准模板库)中的各种算法(如
std::sort
)都是泛型算法。 -
数据结构的通用性:泛型编程广泛应用于实现通用的数据结构,比如链表、栈、队列、哈希表等。这些数据结构通过模板可以存储任意类型的数据。
二、模板
在C++中,泛型编程几乎完全依赖于模板,C++的模板机制提供了一种非常强大的工具,可以在编译时根据类型自动生成代码,从而实现类型的通用性。我们在学习C语言的时候,大家应该都写过Swap函数,这是一种用来交换两个值的函数,但是我们在C语言阶段,实现这种函数每实现一个函数只能针对一种数据类型,如果有多个数据类型的数据要进行交换,那么我们就要实现多个函数,在C语言阶段我们每实现一种函数就要为其命名一个函数名否则就会发生重命名的错误,在C++阶段,我们学习了函数重载,我们可以设计它们的函数名相同,函数参数不同,但是尽管这样,大量相似的代码重复写,属实有点冗长了,那么如何写一个简洁的函数,能够实现不同类型的数据进行交换呢?
C++中的模板是一种强大的工具,允许程序员编写通用代码,可以与多种数据类型一起使用,而不需要为每种数据类型都编写不同的代码。模板分为两种主要类型:
- 函数模板(Function Template)
- 类模板(Class Template)
函数模板
函数模板就像一个函数家族,这个家族里面有各种各样的人,该函数模板与参数无关,只有才函数初始化时,我们传递实参给它,然后它根据参数的类型来产生函数的特定类型版本。意思就是,我们设计好一个模子,然后我们需要具体什么样子的,我们继续给它提要求,然后它给我们创造出一个最符合我们需求的东西。
函数模板格式
template <typename T1,typename T2...,typename Tn>
返回值类型 函数名(参数列表){}
template <typename T>
T add(T a, T b)
{
return a + b;
}
template的中文意思就是模板,typename的中文意思就是类型名字,这里我们还可以使用class来替换它,但是我们要记住我们不能使用struct去替换它们)
函数模板的原理
先来看看我们C语言阶段来实现不同类型的Swap函数
//1.int类型
void Swapi(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
//2.double类型
void Swapd(double& left, double& right)
{
double tmp = left;
left = right;
right = tmp;
}
//3.char类型
void Swapc(char& left, char& right)
{
char tmp = left;
left = right;
right = tmp;
}
再来看看我们使用C++中的函数模板来实现的Swap函数
template <typename T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
通过上面那两组的代码进行比较,我们可以很清楚的看出,上面几个函数都是很相似的,基本上函数体函数名都差不多,不过是改了个函数参数和数据类型。没错就是我们所看到的那样,那些函数基本都差不多,我们每次调用不同类型的函数改一个参数不就行了嘛。函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应 类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
函数模板的实例化
我们把编译器根据传入实参类型来推演生成相应的类型叫做函数模板的实例化。模板的实例化又分为:隐式实例化和显示实例化
1.隐式实例化
隐式实例化是让编译器根据传入的实参类型自己推演出来对应的函数类型
template <class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
int a = 10,b=20;
double m = 1.2, n = 2.3;
cout << Add(a, b) << endl;
cout << Add(m,n) << endl;
/*
cout << Add(a, n) << endl;
*我们传入两种不同数据类型的参数,编译器是无法进行隐式实例化的
* 因为我们编译器是根据我们传入的参数进行隐式实例化的,我们现在一次性传两种类型,编译器无法确定
* 实例化哪种类型的函数,于是索性就报错了
* 对于这种情况,我们可以使用强制类型转换,使其只有一种数据类型,那么就可以进行隐式实例化了
*/
cout << Add(a, (int)n) << endl;
cout << Add((double)a, n) << endl;
return 0;
}
2.显示实例化
显示实例化,是在我们定义的函数模板名后面加上一个<数据类型>来指定函数模板实例化的参数类型
template <class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
int a = 10,b=20;
double m = 1.2, n = 2.3;
cout << Add<int>(a, m) << endl;
cout << Add<double>(b, n) << endl;
//在显示实例化中,我们指明了函数类型,编译会进行隐式类型转换,如果无法进行转换,编译器就会发生报错
return 0;
}
函数模板的匹配原则
其实函数模板并非唯一的,它就像函数重载一样,可以有好多个函数模板,那么在有好几个函数模板/函数的时候应该选择调用谁呢?这其中也有匹配的原则。
//1
int Add(int left, int right)
{
return left + right;
}
//2
template <class T>
T Add(T left, T right)
{
return (left + right)*5;
}
//3
template <class T1, class T2>
T1 Add( T1 left, T2 right)
{
return (left + right)*10;
}
//进行一个函数匹配的测试
int main()
{
int a = 6, b = 4;
double m = 2.1, n = 2.4;
//这里匹配的规则就是:如果有现成的函数就直接调用现成的函数,不调用函数模板中的函数
//如果现成的函数中没有对应参数的函数就调用函数模板中的函数,不同的函数模板根据函数参数进行调用
cout << Add(a, b) << endl;//1 10
cout << Add(m, n) << endl;//2 22.5
cout << Add(a, m) << endl;//3 81
return 0;
}
我们使用上面三组比较经典的函数/函数模板进行匹配测试,上面三组分别是我们自己显示实现的一个具体的函数,另外两个则是函数模板,但是两个函数模板的参数不同,因此它们两个是可以共存的。从上面的运行的结果我们可以看出,传递不同的参数,会调用不同的函数/函数模板。
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数;
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板;
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
说到这里可能会有人来问,我们使用函数模板推演生成的函数如果和我们显示实现的函数类型一样的怎么办,函数模板不过是在我们显示实现的函数的基础上使参数类型自由化了,现在参数类型和我们显示实现的函数都一样,那么两个函数不就一样了嘛,会不会发生一些冲突呢?这个我们其实不用担心的,虽然我们的模板函数于我们显示实现的函数很像,但是编译器在编译时会做一些手脚的(比如我们如果使用函数模板来实例化一个int类型的函数,编译器会将它的函数名设置为Add<int>,与我们自己显示实现的函数还是有所区别的)
类模板
类模板与我们上面的函数模板很像,我们定义一个类模板,可以使他能够与不同类型的数据一起工作,就比如我们之前实现的Stack类,我们在实现类之前会使用typedef来定义一种类型为StTypeData,为的就是我们后面如果想让栈类的数据换成其他数据类型的,只要在这里修改就行了。但是这样做还是只能够在栈类中放一种类型,如果我们想让这个Stack类可以实例化为使用不同类型元素的对象,我们就可以使用类模板了。
类模板的格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
类模板就是在类格式上面加上一个模板和参数,在类内成员的定义中如果有关于参数类型的都要改,这里要注意一下。
类模板的实例化
类模板实例化与函数模板实例化不同,没有什么隐式实例化还是显示实例化之分,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
{
arr = new T[capacity];
_capacity = capacity;
_top = 0;
if (arr == nullptr)
{
perror("new fail");
}
}
//我们写成员函数可以将函数的声明与定义分开写(但是最好不用分文件写,可能会导致编译错误)
void Push(const T& data);
void print(Stack& st);
private:
T* arr;
int _capacity;
int _top;
};
//函数的定义我们在类外写的时候,要指明类域,我们写类域的时候要类名<类型参数>一起写
//另外我们还要写下面的模板参数的声明,不然会发生无法定义参数T的报错
template<class T>
void Stack<T>::Push(const T& data)
{
if (_top >= _capacity)
{
cerr << "Stack overflow" << endl;
return;
}
arr[_top++] = data;
}
template<class T>
void Stack<T>::print(Stack&st)
{
for (T i = (T)0; i < (T)st._top; i++)
{
cout << st._top << " " << endl;
}
}
int main()
{
//我们要注意:Stack是类名,Stack<int>才是类型
Stack<int>st1;
Stack<double>st2;
Stack<char>st3;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
st1.print(st1);
st2.Push(1.1);
st2.Push(2.1);
st2.Push(3.1);
st2.Push(4.1);
st2.print(st2);
st3.Push('a');
st3.Push('b');
st3.Push('c');
st3.Push('d');
st3.print(st3);
return 0;
}
三、STL
STL(Standard Template Library,标准模板库)是C++编程语言中的一个重要库,它提供了一组通用的、模板化的数据结构和算法,用于处理常见的编程任务。STL旨在提高程序开发效率,减少代码重复,确保代码的可重用性和可维护性。如果你是学习C++的,如果别人问你知不知道STL,你说不知道,那别人可得笑话你了,STL在C++中是一个极其重要的部分,换句话来说就是,STL是C++的精华,这节我们只是简单地介绍一下STL,在后续的学习中,我们会慢慢地去学习STL的。
STL的组成
STL由以下六个主要部分组成:
-
容器(Containers): 容器是STL中的数据结构,用来存储对象或元素。STL提供了多种类型的容器,包括顺序容器(如
vector
,deque
,list
,array
)和关联容器(如set
,map
,unordered_set
,unordered_map
)等。每种容器适用于不同的用途和需求。 -
算法(Algorithms): STL提供了许多常用的算法,如排序(
sort
)、查找(find
)、拷贝(copy
)、删除(remove
)等。算法是泛型的,可以与各种容器类型配合使用,这样同一算法可以作用于不同类型的数据结构。 -
迭代器(Iterators): 迭代器是用来遍历容器中元素的对象,它提供了一种统一的方式来访问不同类型的容器。迭代器类似于指针,可以通过它们访问容器中的元素。STL中的常用迭代器类型包括
begin()
,end()
,rbegin()
,rend()
等。 -
仿函数(函数对象)(Function Objects): 函数对象是可以像普通函数一样调用的对象,它们通常是通过重载
operator()
来实现的。函数对象可以与算法一起使用,提供比普通函数更高的灵活性和效率。STL提供了一些标准的函数对象,如less
,greater
,equal_to
等。 -
配接器(Adapters): 配接器是STL中的一个特性,它允许在不修改容器或算法的情况下,为容器或算法提供不同的接口。配接器包括:
- 容器适配器:如
stack
,queue
,priority_queue
,这些适配器基于底层容器(如deque
或vector
)提供了不同的接口。 - 迭代器适配器:如
reverse_iterator
,用于改变迭代器的方向。 - 函数对象适配器:如
bind
,not1
,not2
,用于修改或组合函数对象。
- 容器适配器:如
-
空间配置器(分配器)(Allocators): 分配器是STL中用于管理内存的组件,它们为容器提供内存分配和释放的功能。STL默认使用一个标准的分配器,但你也可以自定义分配器来优化内存管理,尤其在一些特殊场景下,如高性能应用。
这六个部分共同组成了STL,为C++程序员提供了强大且灵活的工具,能够高效地处理数据结构和算法。