个人主页:救赎小恶魔
欢迎大家来到小恶魔频道
好久不见,甚是想念
今天我们要深入讲述C++内存管理
目录
引言:
模板
1. 泛型编程
2. 模板函数
2.1函数模板的原理
2.2模板函数的实例化
2.3函数模板的匹配
3.类模板
STL
STL 的主要组成部分
STL 的优点
使用 STL 的注意事项
引言:
从这一章开始,我们将正式认识C++的一些模板,这也就是C++比C语言的进阶之处
废话不多说,开始讲解 。
模板
1. 泛型编程
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
......
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
答案是可以的
这里就需要引进一个泛型编程了
C++的泛型编程是一种编程范式,它允许程序员编写与类型无关的代码,这些代码可以在多种数据类型上工作,而无需为每个数据类型都编写单独的函数或类。这种特性主要通过模板(templates)来实现。
模板是泛型编程的核心。模板可以是函数模板(function templates)或类模板(class templates)。
2. 模板函数
模板函数的定义:
模板函数使用
template
关键字来声明类型参数。类型参数通常被包含在尖括号<>
中,并且用某种标识符(如T
、typename Type
等)来表示。
其基本结构是:
template <typename T>
T functionName(T parameter) {
// 函数实现
}
下面是一个简单的模板函数的例子,该函数返回两个值的较大者:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
又或者我们写一下上边的交换函数
template <typename T>
void swap(T& a, T& b)
{
T ret = a;
a = b;
b = ret;
}
在这里,typename T
定义了一个类型参数,它在函数模板被实例化时将被具体的类型替换。例如,如果你用 int
类型实例化该模板,编译器将生成一个接受 int
参数并返回 int
类型值的函数,T
代表类型
当我们拥有了模板之后,我们就不用再去写其他类型的交换函数了
template <typename T>
void Swap(T& a, T& b)
{
T ret = a;
a = b;
b = ret;
}
int main()
{
int a, b;
double c, d;
a = 1, b = 2;
c = 3.5, d = 4.5;
cout << a << endl << b << endl << c << endl << d << endl;
cout << endl;
Swap(a, b);
Swap(c,d);
cout << a << endl << b << endl << c << endl << d << endl;
return 0;
}
注意:
typename
是用来定义模板参数关键字,也可以使用class
(切记:不能使用struct代替class)
2.1函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模 板就是将本来应该我们做的重复的事情交给了编译器
所以这里调用的并不是我们的模版void Swap(T& left, T& right)
,编译器会根据我们的调用进行类型推导 ,然后调用所推导的模板函数。
这个函数是编译器根据函数模版和需要的类型生成的,这个过程是编译器实现的。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此
2.2模板函数的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化
隐式实例化:让编译器根据实参推演模板参数的实际类型:
template <typename T>
T Add(T& a, T& b)
{
return a + b;
}
int main()
{
int a = 1, b = 2;
double c = 1.0, d = 3.0;
cout << Add(a, b) << endl;
cout << Add(c, d) << endl;
return 0;
}
像这种,我们的类型定义为T就是隐式实例化
但如果我们调用
Add(a,d);
这样能实现吗?
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int
或者 double
类型而报错,也就是所谓的T不明确
注意:在模板中,编译器一般不会进行类型转换操作
此时有两种处理方式:
- 用户自己来强制转化
- 使用显式实例化
强制转换:
Add(a, (int)d);
Add((double)a,b);
显示实例化:
template <typename T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
2.3函数模板的匹配
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
int Add(int a, int b)
{
return a + b;
}
template <typename T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
//int a = 10;
//double b = 20.0;
显式实例
Add(1, 2);
Add<int>(a, b);
return 0;
}
3.类模板
类模板这玩意挺好用的
我们在学习C语言的时候,需要自己去编写栈,队列等等
而在C++中我们有了类模板就可以直接去调用了
template<class T>
class Stack
{
public:
Stack(int = 10)
: _a(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
void Push(const T& x)
{}
~Stack();
private:
T* _a;
int _top;
int _capacity;
};
注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>
,然后将实例化的类型放在<>
中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
Stack<int> s1;
Stack<double> s2;
Stack类名,Stack<int>
才是类型
使用析构函数演示:在类中声明,在类外定义
template <class T>
Stack<T>::~Stack()
{
if(_a)
delete[] _a;
_top = _capacity = 0;
}
STL
C++ 的标准模板库(Standard Template Library, STL)是一个包含了大量通用模板类和函数的库,这些模板类和函数可以用于执行常见的数据结构和算法操作。STL 是 C++ 标准库的重要组成部分,为程序员提供了极大的便利和灵活性。
STL 的主要组成部分
STL 主要由以下几个部分组成:
- 容器(Containers):
- 序列容器:如
vector
、list
、deque
、forward_list
、array
、string
(尽管string
主要用于处理文本,但它也提供了类似于序列容器的接口)。 - 关联容器:如
set
、multiset
、map
、multimap
,它们基于键值对进行存储,并提供了快速的查找操作。 - 容器适配器:如
stack
、queue
、priority_queue
,它们是对其他容器的封装,提供了特定的接口和行为。
- 序列容器:如
- 迭代器(Iterators):
- 迭代器是 STL 中的核心概念,它提供了一种访问容器中元素的方式。迭代器类似于指针,但比指针更强大、更安全。STL 中的所有容器都提供了迭代器接口。
- 算法(Algorithms):
- STL 提供了大量的通用算法,如排序、查找、复制、替换等。这些算法可以应用于任何支持迭代器接口的容器。
- 函数对象(Functors):
- 函数对象(也称为仿函数)是重载了
operator()
的类对象,可以像函数一样被调用。在 STL 中,函数对象经常作为算法的参数,用于定义算法的行为。
- 函数对象(也称为仿函数)是重载了
- 内存分配器(Allocators):
- 内存分配器负责在容器中分配和释放内存。虽然大多数程序员不需要直接使用内存分配器,但 STL 允许你通过自定义分配器来改变容器的内存管理策略。
STL 的优点
- 可重用性:STL 提供了大量的通用模板类和函数,可以在不同的项目中重复使用。
- 高效性:STL 中的容器和算法都经过了精心设计和优化,可以在大多数情况下提供高效的性能。
- 灵活性:STL 的模板特性使得它可以处理多种数据类型,包括基本数据类型、自定义数据类型和指针类型。
- 安全性:STL 中的迭代器提供了对容器的安全访问,避免了直接使用指针可能导致的错误。
- 可扩展性:你可以通过自定义容器、迭代器、算法和分配器来扩展 STL 的功能。
使用 STL 的注意事项
- 理解 STL 的基本概念:在使用 STL 之前,你需要理解其基本概念,如容器、迭代器、算法等。
- 注意容器的内存管理:虽然 STL 的容器会自动管理内存,但你需要了解它们的内存管理策略,以避免内存泄漏和其他问题。
- 选择合适的容器和算法:在选择容器和算法时,你需要考虑它们的性能、内存使用和易用性等因素。
- 避免不必要的复制:在 STL 中,复制操作可能会非常昂贵。因此,你需要避免不必要的复制操作,例如使用引用传递参数而不是值传递。
- 注意迭代器失效问题:在 STL 中,迭代器的有效性可能会因为容器的修改而失效。因此,你需要在使用迭代器时格外小心。
累了,今天就到这里了