目录
前言:
????????在C++中有一套属于自己的内存管理方式,即new(申请一块空间),delete(归还一块空间)。通过操作new和delete,就可以进行堆上的动态内存管理,他们的用法与c语言中的malloc和free相似,都是申请一块空间后,用一个指针来接收这块空间的地址,归还空间就是用delete/free“修饰”该指针。不同的是:new和delete是操作符,而malloc和free是函数。
????????C++之所以引出new和delete的概念,是因为当new/delete的空间类型是一个自定义类型时,会自动调用该类型的构造函数和析构函数,而malloc和free不会,因此虽然C++中依然可以使用c语言的内容管理方式,但是使用起来会很麻烦。
? ? ? ? new作为操作符,用法就比malloc更简单了,因为malloc是一个函数,malloc需要传参,强制类型转换,以及对申请结果的检查。而new使用起来非常简单,格式为:new+类型,就可以申请一块空间,注意用来接收该空间地址的指针类型必须和申请的空间类型保持一致。
? ? ? ? new申请空间代码如下:
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int;//申请一块int类型大小的空间
*p1 = 100;//解引用找到该空间并赋值
cout << *p1 << endl;
delete p1;//释放申请的空间
return 0;
}
? ? ? ? 运行结果:
? ? ? ? new可以在申请空间的同时,直接对该空间进行初始化赋值,即在申请的空间类型后面加‘()’,括号中给一个数值作为该空间所存储的值。
? ? ? ? new申请空间并且初始化代码如下:
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int(1001);//申请一块int类型大小的空间,并初始化为1001
cout << *p1 << endl;
char* p2 = new char('a');//申请一块char类型大小的空间,并初始化为‘a’
cout << *p2 << endl;
delete p1;//释放p1指向的空间
delete p2;//释放p2指向的空间
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 如果要申请一块连续的空间,malloc一般是用sizeof计算出要申请的空间大小。而new的方式是在申请的空间类型后面加上‘[]’方括号,方括号中填的数值表示要申请多少个该类型的空间。
? ? ? ? 用new申请一块连续空间的代码如下:
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int[3];//申请一块大小为12字节的空间
//可以把该空间看成一个int类型的数组,并对该空间进行赋值
p1[0] = 2;
p1[1] = 3;
p1[2] = 4;
//打印该数组的各个元素
for (size_t i = 0; i < 3; i++)
{
cout << p1[i] << endl;
}
delete[] p1;//释放p1指向的空间
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 注意这里的'[]'和前面初始化用的‘()’要区分开,并且释放空间的时候要加'[]’。
? ? ? ? 申请一块连续的空间也能在申请的同时对其进行初始化,只不过之前用于初始化的小括号'()'换成了大括号‘{}'。
? ? ? ? 示例代码如下:
#include<iostream>
using namespace std;
int main()
{
//申请一块大小为12字节的空间,并且初始化为1,2,3
int* p1 = new int[3]{1,2,3};
//打印该数组中的数据
for (size_t i = 0; i < 3; i++)
{
cout << p1[i] << endl;
}
delete[] p1;//释放p1指向的空间
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 上文提到new/delete的区域如果是一块自定义类型的空间,那么会调用该自定义类型的构造函数和析构函数,而malloc/free则不会,这也是为什么在C++中会经常使用new/delete来管理内存。
? ? ? ? 体现区别的代码如下:
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;//进入构造函数打印这句代码
}
~A()
{
cout << "~A()" << endl;//进入析构函数打印这句代码
}
private:
int _a;
};
int main()
{
A* p1 = new A(6);
//A* p2 = (A*)malloc(sizeof(A));
delete p1;
//free(p2);
return 0;
}
? ? ? ? ?运行结果:
? ? ? ? 从结果可以发现,只有使用new和delete才会调用类A的构造函数和析构函数,而使用malloc和free申请空间和销毁空间时不会调用该类型的构造和析构。
? ? ? ??new/delete与malloc/free的总结:
? ? ? ?
????????相同点:都是在堆上申请的,并且都需要手动申请和手动释放。
? ? ? ? 不同点:
? ? ? ? 1、malloc/free是函数,而new/delete是操作符。
? ? ? ? 2、malloc/free对自定义类型的空间操作时不会调用该类型的构造和析构函数,而new/delete会自动调用。
? ? ? ? 3、malloc需要通过sizeof计算出申请空间的大小,而new直接在[]中写上数值即可,系统会根据类型和数值开辟空间。
? ? ? ? 4、malloc的返回类型为void*因此需要强转,而new直接在后面写上空间类型,并且必须用与之匹配的指针接收。
? ? ? ? 5、malloc要进行返回值的检查,判断返回的指针是否为空,而new不需要,new需要在主函数中捕获异常(一般情况new是不会申请空间失败的)。
? ? ? ? 6、malloc不能对空间进行初始化,而new可以在申请空间的同时对空间进行初始化。
? ? ? ? 一般而言,使用new申请的空间,释放时要使用delete与之匹配,malloc和free既是如此。那如果相互交叉使用会出现什么结果呢。如果是对内置类型的处理,那么相互交叉使用是没有问题的,原因就是delete的底层实现实际上还是调用的是free。
? ? ? ? 对内置类型的处理代码如下:
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int[3];
free(p1);//可以释放申请的12个字节的空间
return 0;
}
? ? ? ? 上述代码是可以正常跑的,new和delete的底层调用图如下:
????????如果是对自定义类型进行new申请空间用free释放,结果能否正常运行呢?
? ? ? ? 示例代码如下:
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A* p1 = new A(6);//用new申请的空间
//A* p2 = (A*)malloc(sizeof(A));
free (p1);//用free释放
//free(p2);
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 可以看到虽然没有自动调用析构函数,但是程序是可以正常运行的。因此可以得出一个结论:由于delete和new的底层实现还是malloc和free,只是new和delete相比于malloc和free会自动调用构造和析构,也就是如果申请的空间中没有指向其他新申请的空间的指针(即该空间不涉及其他空间资源),则new和free可以交叉使用,因为没有涉及到空间资源的问题,不调用析构函数也无所谓。
? ? ? ? 另一种情况是自定义类型里面的成员变量自己申请了一块新的空间,这时候用new申请了一块该自定义类型的空间,并且用的是free释放,则不会处理该空间里申请的新空间。
?????????示例代码如下:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int n = 4)
:_arr(new int[n])//成员_arr涉及到空间资源问题
,_top(0)
,_capacity(n)
{
cout << "Stack(int n = 4)" << endl;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _arr;
_arr = nullptr;
_top = _capacity = 0;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
Stack* pst1 = new Stack(10);
//用delete释放pst1,并调用Stack中的析构函数释放_arr申请的空间
delete pst1;
return 0;
}
? ? ? ? 从上文可以发现,连续空间用的是delete []形式进行释放,那么‘[]’的作用是什么呢?比如申请了3个类型为自定义类型的空间,可以理解成申请了一个自定义类型的数组,数组里的每个元素的类型是自定义类型,则系统会调用三次构造函数来对这三个元素进行初始化。当用delete释放该数组时,系统又会调用这三个元素的析构函数。
? ? ? ? 示例代码如下:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int n = 4)
:_arr(new int[n])//成员_arr涉及到空间资源问题
,_top(0)
,_capacity(n)
{
cout << "Stack(int n = 4)" << endl;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _arr;
_arr = nullptr;
_top = _capacity = 0;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
Stack* pst1 = new Stack[3];
//用delete释放pst1,并调用Stack中的析构函数释放_arr申请的空间
delete[] pst1;
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 可以发现,确实调用了三次构造函数和三次析构函数。但是如果把‘[]’去掉,直接用delete pst1进行释放,编译就会直接报错。
? ? ? ? ?而且只调用了一次析构函数,那么可能是后面两次析构函数没有调用导致空间没有释放干净导致报错的,但是在C++中申请了空间即使不进行释放是不会报错的,那么这里报错的原因是什么呢?
? ? ? ? 首先如果用new申请了一块连续的空间,编译器为了能知道释放的时候要调用几个析构函数,会在该空间的前面自己申请一块用于记录调用析构函数个数的空间,具体示意图如下:
? ? ? ? 当我们使用delete []释放pst1时,pst1的位置就会发生变化:
? ? ? ? 不一定申请了一块连续的空间就必须得用delete []进行释放,比如下面代码申请了一块连续的空间,但是没有使用delete []释放,结果也不会报错。
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;
}
private:
int _a;
};
int main()
{
A* p1 = new A[3];//申请了一块连续的空间
delete p1;//用的是delete释放
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 正常运行的前提是我们没有自己写析构函数,用的是系统自己生成的析构函数进行析构的,原因在于系统自己生成的析构函数会判断类A无需调用析构函数,因此就不会申请那块用于记录调用析构函数次数的空间,所以直接从pst1指向的位置释放就没有问题。
? ? ? ? 定位new的作用就是对一块已经存在的空间(并且该空间是未被初始化的)进行初始化,并且格式必须按照:new+(指针变量)+类型+(该类型初始化列表)。
????????定位new一般是初始化从内存池里分配出来的内存(从内存池中获取内存的消耗比直接从堆上获取内存的消耗要小),因为从内存池中分出来的内存是没有被初始化的,而且如果是自定义类型的空间,就可以使用定位new对其进行初始化。定位new的主要特点就是可以显示调用构造函数(即手动调用构造函数)。
? ? ? ? 示例代码如下:
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
//private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
if (p1 == nullptr)
{
perror("malloc");
return 0;
}
new(p1)A(10);//手动调用p1指向空间的构造函数,并初始化为10
cout << p1->_a << endl;
//释放方法一:
p1->~A();//可以手动调用p1指向空间的析构函数
free (p1);//释放p1指向的空间
//释放方法二:
//delete p1;//也可以用delete进行释放
return 0;
}
? ? ? ? 运行结果:
? ? ? ? 以上就是new和delete的讲解,其实在使用new和delete的时候只要一一对应的使用就不会出问题,即用new申请的空间用delete释放,用new申请连续的空间用delete []释放。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!谢谢大家!!