C++内存管理与模板

C++内存管理与模板

前言:

C++的内存管理和C语言中动态内存分配是相似的,在这一篇我们会学到更符合面向对象的new和delete;

学习模板知识,是我们迈入STL的最后一步,届时可以飞速转为C++选手;

一.new和delete基本用法

在这里插入图片描述

new后面加类型会开辟类型大小的空间,p1维护一个整型的空间,使用圆括号进行初始化;单纯用new在堆上开辟的空间使用delete释放。

parr维护一个5个double元素的数组,使用{}为数组按顺序初始化,未完全初始化的默认给0;使用new[]开辟空间的使用delete[]释放,具体原因后面讲。

以上对内置类型进行堆空间分配和释放和C语言使用malloc和free本质上没有区别,只是用法上不太一样,比如不需要计算类型大小、无需强转。

new和delete解决的问题不在于内置类型,而是自定义类型,请看下面代码:

在这里插入图片描述

在这里插入图片描述

new在为类对象开辟空间的时候,会调用类对象的构造函数初始化对象,这是为了符合“刚创建的对象最好一开始进行初始化”,因此new有两个功能:1.开辟空间;2.调用构造函数;

对于自定义类型会去调用构造函数,对于内置类型不会,因为没有构造函数可以掉,在C++中很多都是针对自定义类型的,这是面向对象的特征。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

delete在释放new为类对象开辟的空间之前会先调用析构函数进行资源清理,然后再释放对象,也就是说delete的功能是:1.清理资源;2.释放空间;

接下来我们讲一个复杂一点的:创建栈对象

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_a = new int[capacity];//new开辟空间失败不返回空指针
		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a;
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack* ptr = new Stack;
	delete ptr;
	return 0;
}

在这里插入图片描述

到这里,new和delete使用我们讲清楚了,那么new和delete到底是什么呢?它是如何完成这些工作的,让我们来看看new和delete的底层。

二.底层实现

new和delete是C++里的两个操作符,基本用法就是上面讲的。在C语言中malloc是用来开辟空间的,但是如果失败返回空指针不符合C++面向对象的惯常做法,此时祖师爷封装了这么一对全局函数:

在这里插入图片描述

operator new的主逻辑是调用C库中的malloc函数,然后对malloc开辟空间失败的情况进行封装,从返回空指针变成更符合面向对象的抛异常。

因此读者可以这么理解:operator new本质就是一个封装版的malloc,对malloc开辟空间失败的情况做了处理。

讲到这里,相信大家已经猜到operator delete的本质是free了。没错,operator delete单纯是用来配对operator new,和free本质上没有区别。

读者:好,博主,我知道了!这是两个全局函数,那它和new、delete这两个操作符有什么关系呢?

在这里插入图片描述

当我们使用new这个操作符的时候,翻译成汇编看到:编译器会自动去调用operator new这个函数,还有调用构造函数的。delete也是,会先调用析构函数,再调用operator delete。

到这里,new和delete再也不是很神秘的东西了,底层的operator new 和 operator delete就是C语言中的malloc和free。

在这里插入图片描述

在这里插入图片描述

我们看到如果在使用new[]的时候,调用operator new[]这个函数,但是进到这个函数内部,立马调用operator new,因此这个带[]的new函数仅是为了对称。

开辟5个栈对象,总大小应该是60才对,为什么是64呢?

在这里插入图片描述

我们看ptr的指针是指向E67C的,E678是5,我们连蒙带猜的把这个整型空间算在ptr里的话就算是指向一块64字节大小的空间。

没错,实际上对于new[]的时候,会多开辟一个整型空间用来存储对象个数。用途是:用来给delete[]使用,这也是delete[]的[]里不需要写析构对象个数的原因。

那么到这里我们就能明白为什么new/delete、new[]/delete[]要搭配使用了,因为如果使用new[]和delete,此时delete并不像deletep[]一样会释放指针-4的位置,那么就会导致开辟的空间部分释放,运行报错!

同理,malloc/free与new/delete最好也不要混着使用,老老实实配套使用就不会坑自己。

三.定位new

int main()
{
	//以下这两句组合相当于: Stack* p1 = new Stack(10);

	//1.开辟空间
	Stack* p1 = (Stack*)operator new(sizeof(Stack));
	//2.调用构造  定位new:显示调用构造函数的方法
	new(p1)Stack(10);

	//以下两句组合等同于:delete p1;

	//3.清理资源 C++中可以显示调用析构函数
	p1->~Stack();
	//4.释放空间
	operator delete(p1);

	return 0;
}

在这里插入图片描述

定位new的使用是:new(指针)类型(初始化列表)这样,就可以显示调用构造函数了,在内存池技术中发挥用武之地。

在这里插入图片描述

在这里插入图片描述

以上就是C++中的内存管理学习,我们接着往下进入模板的学习!

四.模板

4.1函数模板

在生活中,处处有模板的身影,比如语文答题模板、活字印刷技术等等很多。模板的出现,让相似的问题得以快速的解决,在计算机中,也有模板的身影:

//这两个交换函数除了类型以外,没有任何不同
void Swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

void Swap(double& left, double& right)
{
	double tmp = left;
	left = right;
	right = tmp;
}

对于这种只有类型不同,逻辑一样的函数需要写两个函数,祖师爷早就看不惯了:

//函数模板
template<typename T>
void Swap(T& left, T& right)
{
    //模板参数T
	T tmp = left;
	left = right;
	right = tmp;
}

int main()
{
	int a = 10, b = 20;
	double c = 1.1, d = 2.2;
	Swap(a, b);
	Swap(c, d);
	return 0;
}

模板参数T指的是类型,使用tmplate<typename 类型名>类型名可以任意起,因为是定义类型的,博主的习惯是取为T(type)。那么这个模板有什么用?请看下面代码:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

从上图看出不管是int还是double调用,调试的时候都是进入函数模板,表面上看起来像走了模板函数一样。

但实际上Swap(a,b)的时候,函数模板通过实参的类型,编译器推断模板参数T是int,然后编译器生成了一份整型交换的Swap函数并调用,这就是函数模板的参数推演。

所以Swap(a,b)和Swap(c,d)调用的不是同一个函数(它们地址不同),是调用编译器确定的函数参数T照函数模板生成的那份函数代码。

在这里插入图片描述

可以使用多个模板参数,多个模板参数的定义和多个函数参数的的定义是一样的,typename和class可以互换,博主习惯使用class。举个例子就是下面的代码:

template<class T1, class T2>
4.2调用选择

在这里插入图片描述

当模板和现有的函数存在时,如果现有的函数是合适的,那么就不会调用编译器通过模板生成的,而是调用现成的。

在这里插入图片描述

在这里插入图片描述

没有现成的函数就让编译器通过模板生成一个,调用生成的函数。此外,如果没有函数模板,能通过隐式类型转换符合参数类型的现成函数可以凑合被调用。

在这里插入图片描述

在这里插入图片描述

以上就是编译器根据函数模板生成的与现成函数的调取情况,可能不同编译器的具体情况不太一样,博主使用的是VS2019。

前面编译器根据实参推演生成的函数都是被动的,我们可以主动给模板参数类型,让编译器生成对应的函数,下面请看显示实例化:

在这里插入图片描述

这种函数的参数与模板参数没有关系或者函数没有参数的,就不能通过传递实参让编译器根据实参的类型,推演生成对应类型的函数代码,而要程序员自己实例化。

以上是实例化的方式,也就是语法规则,记住就好了。

4.3类模板

类模板是用来对只有类型不同的代码简化方法,请看下面代码:

template<class T>
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = new T[capacity];
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		delete[] _a;
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
private:
	T* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack<int> st1;//整型栈
	Stack<double> st2;//double类型栈
	return 0;
}

当我们定义了一个栈的类模板后,对于存储不同数据类型的栈不再需要写两份(只有数据类型不同)代码了,通过显示类模板实例化,让编译器为我们生成即可!

4.4声明定义分离
template<class T>
class Stack
{
public:
	//Stack是类名,构造函数是和类名相同的
	Stack(int capacity = 4);
	~Stack()
	{
		delete[] _a;
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
private:
	T* _a;
	int _top;
	int _capacity;
};

//声明定义分离的时候,模板参数要跟上
template<class T>
Stack<T>::Stack(int capacity)
{
	_a = new T[capacity];
	_top = 0;
	_capacity = capacity;
}

int main()
{
	//它们是同一个类模板实例化出来的栈,但不是同一个类型

	Stack<int> st1;//Stack<int>是一个类型 存放整型的栈
	Stack<double> st2;//Stack<double>是另一类型 存放double的栈
	return 0;
}

在这里插入图片描述

类模板中,声明和定义分离,定义指明类型(Stack<T>)这是因为模板参数T在成员函数中是有作用的,即使在某些成员函数中没有使用到T,也需要加上,不能单靠Stack::。

ok,以上就是C++中内存管理和模板的知识学习,希望读者读完有所收获。

  • 30
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

啊苏要学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值