文章目录
- 内存管理
- new和delete
- 函数模板
- 隐式实例化
- 显式实例化
- 类模板
内存管理
有时候我们需要动态的申请内存,比如队列,栈,二叉树等数据结构,我们一开始并不知道要存储多少个数据,也就是不确定究竟要多大的内存,给小了存不下,给大了会造成内存资源的浪费,因此动态的申请内存是必要的,即需要多少申请多少。
那么在C语言阶段我们不是已经学过malloc,calloc,realloc,free这几个动态开辟内存的函数了吗?为什么C++中还要引入新的,其中一个重要原因就是,C语言的不可以对动态开辟的内存进行任意的初始化,特别是对于自定义类型来说。接下来我们引入两个新的关键字,new和delete。
new和delete
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
free(p1);
delete p2;
对于内置类型来说new和delete与malloc和free除了用法上不一样,本质上并没有什么区别,都不会对新开辟的内存进行初始化。
但是通过new可以显式的对他进行初始化
int* p2 = new int(5);
只需后面加上(),括号内给值即可。
class Date
{
private:
int _year;
int _month;
int _day;
};
Date* p3 = (Date*)malloc(sizeof(Date));
Date* p4 = new Date;
free(p3);
delete p4;
对于自定义类型来说malloc只是开辟了一段Date这个对象大小的空间,并没有初始化,而通过new申请空间时它会自动调用这个对象的构造函数对它进行初始化,并且free也仅仅是释放这个对象的空间,如果成员变量中包含在堆上动态申请的空间例如栈,它并不会对栈进行释放,而delete会自动调用析构函数对成员变量中动态开辟的空间进行释放。
如下:
class Stack
{
public:
Stack()
{
}
~Stack()
{
}
private:
int* _a;//成员变量需要在堆上动态申请空间
int _capacity;
int _size;
};
动态申请数组
int* p5 = (int*)malloc(sizeof(int) * 5);
int* p6 = new int[5];
free(p5);
delete[] p6;
对于内置类型的数组而言,在申请空间之后也不会对它进行初始化,但是通过new可以在申请数组的同时对它进行初始化
int* p5 = (int*)malloc(sizeof(int) * 5);
int* p6 = new int[5] {1, 4, 5};
当通过new申请数组时,如果给的初始值不够那么就会用0来初始化其它值。如果不给初始值那么就不会进行初始化。
对于释放通过new开辟的数组空间,要通过delete关键字加上[],再加上指针来完成空间的释放,并且会先调用析构函数。
重要:内置类型对象也有它对应的默认构造和析构函数,只不过用户无法自己去定义。比如我们在delete一个自定义类型对象时,它的成员变量中有一个动态开辟的int类型的数组,在delete时先调用的是这个对象的析构函数,对象的析构函数中调用的是内置类型的析构函数,释放动态申请的空间,两次析构函数不是同一个所以不会发生死循环。
函数模板
函数模板是什么?为什么要有函数模板,比如我们在对两个变量值进行交换时,这两个变量的类型可能为int,也可能为double等等,难道我们要把各种类型的Swap函数都要写一遍吗?只是把参数的类型换了一下,函数框架并没有改变,这样显然是繁琐的,因此C++中引入了模板,模板可以根据你传来的实参进行自动的推导。
模板关键字为:template
#include <iostream>
using namespace std;
//template<class T1, class T2>
template<typename T1, typename T2>
void Swap(T1& x, T2& y)
{
T1 tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 5;
int b = 6;
double c = 7.6;
Swap(a, b);
Swap(a, c);
}
这里的Swap只是一个模板,在我们调用时把实参传过去,模板会自动根据实参类型实例化出对应的函数
Swap(a, b);//实例化为void Swap(int& x, int& y)
Swap(a, c);//实例化为void Swap(int& x, double& y)
初学阶段可以认为class和typename没有区别,但是在C++中我们还是习惯用typename因为见名知意嘛,就是类型名
那么下面模板写成这样对不对?
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
此时就取决于你传递的实参类型了,如果实参类型相同,那么没什么问题
如果实参类型不同比如Swap(a, c);
Swap(a, c);//实例化为void Swap(int& x, double& y)
对于函数体内的T而言它就会出现歧义,是int类型呢还是double类型呢?
对于这种情况有两种解决方法:
隐式实例化
template<typename T>
void Swap(const T& x, const T& y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 5;
int b = 6;
double c = 7.6;
Swap(a, b);
Swap(a, (int)c);
Swap((double)a, c);
}
直接对实参进行强制类型转换,然后再让编译器根据实参推导出模板参数的实际类型。
注意我们这里模板中的形参前都加了一个const,这是为什么呢?
因为我们这里对实参进行了强制类型转换,实际上这里也发生了隐式类型转换,例如(int)c它有double类型转换成了int类型,意识类型转换的过程中又会产生临时变量,所以传过去的实际是实参的拷贝,而拷贝又具有常性,在引用前不加const就会导致权限放大,所以一般情况下我们在不改变形参的情况下尽量都要在前面加上const。
显式实例化
int main()
{
int a = 5;
int b = 6;
double c = 7.6;
Swap<double>(a, c);
return 0;
}
在函数后的<>中指定模板参数的实际类型,这里指定模板参数类型为double
其实显式实例化通过下面的栈更能体现出它的作用,在栈中没有任何函数实参的类型能推断出T的类型,因此我们必须提供显式的模板实参
隐式实例化和显式实例化区别:
隐式实例化是传过去一个参数让编译器根据实参去推模板参数的实际类型,必须要有参数传过来才可确定模板实参类型。
显式实例化是直接指定模板参数的类型,可以没有参数传过来。
类模板
类模板同理,也是给只是类型不同的类抽象出来的模板,比如在数据结构栈中,在C语言阶段我们如果要去存不同类型的数据就要定义出多个不同类型的栈,但是在C++我们可以给定一个模板,然后让编译器这个老大哥帮我们去自动推导要存的数据类型。
#include <iostream>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(const int n);
~Stack();
void StackPush(T x);
void StackPop();
int StackSize();
private:
T* _a;
int _capacity;
size_t _size;
};
template<typename T>
Stack<T>::Stack(const int n)//初始化列表是成员变量+括号, 不是加=等于号
:
_a(new T[n]),
_capacity(n),
_size(0)
{
}
//template<typename T>
//Stack<T>::Stack(const int n)
//{
// T* _a = new T[n];
// int _capacity = n;
// int _size = 0;
//}
template<typename T>
Stack<T>::~Stack()
{
if (_a)
{
delete[] _a;
}
_capacity = _size = 0;
}
template<typename T>
void Stack<T>::StackPush(const T x)
{
if (_size == _capacity)
{
perror("栈满了");
return;
}
_a[_size] = x;
_size++;
}
template<typename T>
void Stack<T>::StackPop()
{
if (StackSize() <= 0)
{
cout << "栈中没有元素" << endl;
return;
}
_size--;
}
template<typename T>
int Stack<T>::StackSize()
{
return _size;
}
int main()
{
Stack<int> S = 5;
S.StackPush(1);
S.StackPush(2);
S.StackPush(3);
S.StackPush(4);
S.StackPush(5);
S.StackPop();
S.StackPop();
S.StackPop();
return 0;
}
1.在类模板中声明和定义可以分离,但是不建议分离到两个不同文件中,一般都放在头文件中
2.如果类模板中的成员函数声明和定义分离,那么在每个分离的函数定义之前我们就要再写一遍关键字 template+模板参数列表,因为编译器不知道这里的T是什么,也不知道去哪里找。
3.在普通类中类名就是类型名,比如之前的日期类Date,它的类型也是Date,但是在类模板中类模板名不等同于类型名,类型名是类名+,如果T已经确定为具体的类型比如int那么它对应的类型名就是类模板名+。
4.在对模板类进行实例化时,我们要指定T的类型,比如上面的栈如果我们要存储的类型是int类型,那么在对它进行实例化时对应的对象类型就是Stack
5.delete先调用的是int的析构函数释放数组空间,再free掉这个对象。所以这里在析构函数中使用delete不会出现死循环,因为两次调用的析构函数不是同一个。