浅析C++中的动态内存分配

在C语言中,我们学到了使用malloc/calloc/relloc来进行内存的动态开辟,用free来完成内存的释放。而在C++中是通过new和delete这两个操作符来实现的。当然C++中也可以使用C语言那套内存管理的方法,毕竟C++是兼容C语言的。

一.首先我们来了解一下new和delete的基本语法。

int main()
{
	int* p1=new int;//开辟空间
	delete p1;//释放空间
	int* p2=new int(10);
	delete p2;
	int* p3=new int[5];//开辟数组空间
	delete[] p3;//释放数组空间
	system("pause");
	return 0;
}   

对于new和delete而言,一般用new和delete动态管理对象,用new[]和delete[]动态管理数组;我们来在内存中观察一下内存开辟的情况:


我们可以看到内存成功开辟并且p2指向的值被初始化为10.

在使用new和delete时,应遵循如下规则:1.不要使用delete来释放不是new分配的内存;

                                                                2.不要使用delete来释放同一块内存两次;

                                                                3.应该使用delete来释放用new动态开辟出来的对象;

                                                                4.应使用delete[]来释放用new[]动态开辟的数组。

                                                                5.对于空指针使用delete是安全的。

二.对于上述规则,年轻的我们总是不喜欢墨守成规,你说不行,我还非得试试,哈哈,那么接下来我们就来研究下不把new和delete来匹配使用看到底会出现什么情况?会崩溃还是会内存泄露??

1.自定义类型

#include<iostream>
using namespace std;
int main()
{
	int* p1=new int;
	delete p1;
	//delete[] p1;
	//free(p1);
	int* p2=new int(7);
	delete p2;
    //delete[] p2;
	//free(p2);
	int* p3=new int[5];
	delete[] p3;
	//delete p3;
	//free(p3);
	int* p4=(int*)malloc(sizeof(int));
	free(p4);
	//delete p4;
	//delete[] p4;
	system("pause");
	return 0;
}

上面每一种内存开辟的方法都有一种正确释放方法和两种不正确的释放方法。我们可能会认为不匹配可能会导致内存泄露或者运行时崩溃,但事实却出乎我们的意料,将上面所有释放方法逐一运行 之后没有发现程序崩溃的情况,我们还是会质疑内存真的正确释放了吗?我们可以通过调试来看一下:


此时我们可以看到内存开辟成功。接下来看一下内存是否正确释放:


我们可以看到内存依然正确的被释放了,也就是说不存在内存泄露问题。这看起来好像对于内置类型来说,不匹配使用并不会出现什么问题。但是为了避免出现不可预知的错误,作为一个程序员,依然要有一个良好的编程规范去正确的使用动态内存的开辟与释放。

2.上面探讨了内置类型动态开辟空间与释放不匹配的情况,那么用自定义类型来开辟空间,不匹配释放的话会出现什么问题吗?

我们来试一试:

class AA{
public:
	AA(size_t size=0)
		:_p(NULL)
		,_size(size)
	{
		if(_size>0)
		{
			_p=new char[size];
		}
		cout<<"AA()"<<endl;
	}
	~AA()
	{
		cout<<"~AA()"<<endl;
		if(_p)
		{
			delete[] _p;
			_p=NULL;
			_size=0;
		}
	}
private:
	char* _p;
	size_t _size;
};
int main()
{
	AA* p1=new AA;
	delete p1;
	//delete[] p1;//崩溃
	//free(p1);
	AA* p2= new AA[10];
	delete[] p2;
	//delete p2;//崩溃
	//free(p2);//崩溃
	AA* p3=(AA*)malloc(sizeof(AA));
	free(p3);
	//delete p3;//程序异常终止
	//delete[] p3;//崩溃
	AA* p4=new AA(10);
	delete p4;
	//free(p4);
	//delete[] p4;//崩溃
    system("pause");
	return 0;}

对于自定义类型,同样4种动态开辟内存的方法,每种都有三种释放内存的可能,经过验证,上面标注出崩溃的语句是会让程序在运行时崩溃的,我们会想,为什么内置类型没有问题,而对于自定义类型会出现这么多崩溃的情况。这就需要去深入的了解new和delete到底是怎样调用的。

第一种:

AA* p1=new AA;
//delete p1;
//delete[] p1;
//free p1;

对于第一个释放方法很明显是正确的,我们还是去看一下它底层是怎么实现的。运行结果为:


经过调试,发现new先去调用了一个operator new的函数去分配空间,然后调用了构造函数初始化对象,delete先去调用了析构函数清理对象,然后调用了operator delete函数释放空间。


这是operator new的函数实现,可以看出这个函数实质上还是调用了malloc函数,如果malloc函数失败,则new也会失败,但是不是用返回值为NULL去表示失败,而是用bad_alloc抛异常的方式说明动态内存开辟失败。换句话说,operator new 实质上是malloc函数的包装版本。同样的,operator delete也去调用了free函数。

第二种:

AA* p2= new AA[10];
	delete[] p2;

这种动态开辟的数组是怎么实现的呢?

class AA{
public:
	AA(size_t size=0)
		:_p(NULL)
		,_size(size)
	{
		if(_size>0)
		{
			_p=new char[size];
		}
		cout<<"AA()"<<endl;
	}
	~AA()
	{
		cout<<"~AA()"<<endl;
		if(_p)
		{
			delete[] _p;
			_p=NULL;
			_size=0;
		}
	}
private:
	char* _p;
	size_t _size;
};
int main()
{
	AA* p2= new AA[6];
	cout<<*((int*)p2-1)<<endl;
	cout<<p2<<endl;
	delete[] p2;
	system("pause");
	return 0;
}

运行结果为:


程序调用了6次构造函数和6次析构,不难想动态开辟了有6个元素的数组,每创建一个自定义元素,就要去调用一次构造函数,自然也需要析构6次,但是我们难以想明白的是构造函数调用6次是因为显示指出了要动态开辟6个元素,但是delete[] p2;并没有显式的指出要调用多少次析构函数。

表达式*((int*)p2-1)计算的是p2的前4个字节位置处所存放的元素。查看内存如下:



经过调试发现,new AA[6]在开辟空间的时候多开辟了4个字节的空间来存储,我们可以看出,p2的地址为:0x00F233D4,前面的地址0x00F233D0处所存放的元素刚好是动态开辟的元素个数。所以delete[]是怎么知道需要调用多少次析构函数的呢?当调用delete[]的时候,需要回去查找开辟的第一个位置处所存放的元素值来判断需要调用多少次析构函数。

接下来理一下刚开始那些不正确释放动态分配的内存崩溃的原因:

int main()
{
	AA* p1=new AA;
	delete p1;//正确
	//delete[] p1;//崩溃,delete[]会从p1前面的4个字节处开始释放,释放位置错误
	//free(p1);
	AA* p2= new AA[10];
	delete[] p2;//正确
	//delete p2;//崩溃,释放位置出错,应该从p2前面的4个字节处开始释放,但是直接从p2位置释放
	//free(p2);//崩溃,原因同上
	AA* p3=(AA*)malloc(sizeof(AA));
	free(p3);//正确
	//delete p3;//如果自定义类型的析构函数什么也没做则不会崩溃,否则会崩溃。
	//delete[] p3;//崩溃,释放位置出错,delete[]会从p3前面的4个字节处开始释放,
       AA* p4=new AA(10);
	delete p4;//正确
	//free(p4);//free在一定程度上可以释放new开辟的内存
	//delete[] p4;//崩溃,释放位置出错,delete[]会从p4前面的4个字节处开始释放,
        system("pause");
	return 0;}

用图来理解下new和delete以及new[]和delete[]的实现原理:


C++中的其他内存管理接口:注意:并非new和delete操作符的重载,未调用构造和析构

void* operator new(size_t size);

void operator delete(size_t size);

void* operator new[](size_t size);

void operator delete[](size_t size);

三.总结

 1、new/delete;new[]/delete[];malloc/free一定要匹配使用,不能混乱搭配,否则会出现内存泄漏或者程序崩溃。

2、new/delete是C++操作符;malloc/free是C/C++标准库的函数。

3、new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理工作;而malloc/free只是动态分配内存空间/释放空间。

4、new/delete可自己计算类型的大小,返回对应类型的指针;malloc/free需要人为计算类型大小且返回值为void*。

5、new开辟空间失败会抛出异常;malloc开辟空间失败会返回0。

6、operator new/delete 是malloc/free的一层封装,并不是new/delete的重载。

7、new[]在开辟空间,如果显式定义了析构函数,new会多开辟4个字节的空间(64位8个字节),用来存放对象的数量,以便delete[]能够准确的调用析构函数。













猜你喜欢

转载自blog.csdn.net/qq_39344902/article/details/79863302