1. c语言中动态内存管理的方式
malloc/calloc/realloc/free
void test()
{
int* p1 = (int*)malloc(sizeof(int));
int* p2 = (int*)calloc(sizeof(int));
int* p3 = (int*)realloc( p2 , sizeof(int));
free(p1);
free(p3);
}
- malloc和calloc都是直接在堆上开辟空间,但是calloc会初始化开辟出来的空间。
calloc = malloc + memset; - realloc是扩容,分为原地扩容和异地扩容。
原地扩容,原来的指针和扩容后的指针指向同一块空间,只需要free新指针即可。
异地扩容,realloc会自动free掉原来空间的指针。最后只需要free掉新指针即可。
2. c++中的动态内存管理
c语言中,每次开辟空间都需要计算空间的大小,还需要对空间进行强制类型转换。
c++中:
new是开辟空间
delete是销毁空间
2.1. new的简单使用
void test()
{
int* p1 = (int*)malloc(sizeof(int));//c++支持c语言
int* p2 = new int;
int* p3 = new int(3);//初始化空间
delete p1;
delete p2'
delete p3;
int* p4 = (int*)malloc(sizeof(int)*3);
int* p5 = new int[3];//创建多个对象
delete[] p5;//多个对象的删除方式
}
new 会自动计算大小,不需要进行强制类型转换。
c++兼容c语言,内置类型的动态申请,用法简化了,功能方面 new 和 malloc 基本一致。
new 支持初始化
new支持开多个空间+初始化
int* p1 = new int(10);//用10初始化
int* p2 = new int[10];//创建10个int类型
多个对象的初始化
int* p3 = new int[10]{1,2,3};
//前三个元素用1,2,3初始化,其余元素用0初始化
int* p4 = new int[10]{};
//数组用0初始化
![在这里插入图片描述](https://img-blog.csdnimg.cn/bc5853f6717d444ca89c6d5ff5bb9a19.png)
c++中多个对象的初始化,写起来类似数组,但是不能像下面的写法
int* p5 = new int[10] = {1,2,3};
int* p6 = new int[10] = {};
2.2. delete的简单使用
delete的作用类似于free
如果开辟的空间只有一个,直接delete即可
如果开辟的空间是个数组,需要使用delete[]
void test()
{
int* p1 =new int;
delete p1;
int* p2 = new int[10];
delete[] p2;
}
这里对应的开辟空间的方式要对应销毁空间的方式。
如果delete与开辟空间方式不匹配,结果是未定义的。
delete[] “[]”内不需要填数据。
2.3. 为什么要设计new
如果只是像上面说的,new是为了简化代码,其实是没有意义的。
new设计的真正目的,是针对自定义类型。
malloc在开辟空间的时候,他只会创造空间,不会对空间进行初始化。
而c++中有构造函数的概念,new在用自定义类型开辟空间的时候,会自动调用构造函数。
#include<iostream>
using namespace std;
class A
{
public:
A(int nn =10)
:_a(a);
(
cout<<"A( int n =10 )"<<endl;
)
~A
{
cout<<"~A()"<<endl;
}
A(const A& a)
:_a(a)
{
cout<<"A(const A& a)"<<endl;
}
A& operator=(const A& a)
{
if(this!=&a)
_a=a._a;
cout<<"A& operator=(const A& a)"<<endl;
return this;
}
int main()
{
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(3);
A* p3 = new A;
free(p1);
delete p2;
delete p3;
return 0;
}
new会在开辟空间的时候,自动调用构造函数
delete在销毁的时候,自动调用析构函数。
如果是malloc和free
malloc开辟空间后需要手动调用初始化,
free销毁前需要手动销毁内部成员。
如果是用new创建多个对象,就会多次调用构造函数 delete 会多次调用析构函数。
这里可以初始化,可以使用隐式类型转化
A* p1 = new A[5]{ 1,2,3,4,5 };
delete[] p1;
A* p2 = new A[10]{ A(1),A(2),A(3)};
delete p2;
如果没有隐式类型转换,我们需要传入对应的对象参数,用对象参数去构造数组中的对象元素。
有了隐式类型转换,用传入的数字,直接构造对象。
- 创建单链表
#include<iostream>
using namespace std;
struct ListNode
{
ListNode(int n =0)
:_val(n)
,_next(nullptr)
{}
~ListNode()
{
_next=nullptr;
)
ListNode* _next;
int _val;
};
int mian()
{
ListNode* n1 = new ListNode(1);
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(3);
ListNode* n4 = new ListNode(4);
ListNode* n5 = new ListNode(5);
n1->_next = n2;
n2->_next = n3;
n3->_next = n4;
n4->_next = n5;
delete n1;
delete n2;
delete n3;
delete n4;
return 0;
}
如果使用malloc开辟空间,我们还需要不断调用创造结点的函数。
new和delete补了malloc和free不能调用构造和析构函数的缺点。
- 堆上开空间,不会自动销毁
这里有个栈的代码
#include<iostream>
using namespace std;
typedef int DateType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
cout << "Stack" << endl;
_array = new DateType[capacity];//new失败了会抛异常,不需要判断空
}
void Push(DateType x)
{
if (_size == _capacity)
{
int newcapacity = (_capacity == 0 ? 4 : 2 * _capacity);
DateType* tmp = (DateType*)realloc(_array,sizeof(DateType) * newcapacity);
if (tmp == nullptr)
{
perror("realloc failed");
exit(-1);
}
_capacity = newcapacity;
_array = tmp;
}
_array[_size++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
private:
DateType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s1;
return 0;
}
按照我们之前的逻辑,如果有下面的函数
Stack* func()
{
int n;
cin >> n;
Stack st();
return &st;
}
这里不能返回,因为自定义类型出了函数作用域会自动调用析构函数。最后返回的栈是已经经过析构函数处理的,返回的是野指针。
但是如果我们需要在这个函数内创建栈,并且将创建好的栈返回到调用函数的地方,我们就可以手动用 new 创建。
Stack* func()
{
int n;
cin>>n;
Stack* pst = new Stack(n);
return pst;
}
这里,我们直接自己new了一块空间,这块空间出了函数作用域不会自己调用析构函数,会保存在堆上,想要销毁这块空间的话,我们需要手动delete。
如果使用malloc和free,就需要我们自己手动对栈内初始化和销毁,相对来说麻烦点。
3. operator new与operator delete函数
3.1. operator new
new 和 delete 是用户在进行动态内存申请和释放的操作符,operator new 和operator delete 是系统提供的全局函数。
operator new实际上是对malloc函数的封装。
operator new 只是库函数中的叫法,operator delete也是,并不是说我们使用的 new 和 delete 就是他们。(这不是运算符重载)
直接使用operator new 用法和malloc一样
int main()
{
Stack* pst = (Stack*)operator new(sizeof(Stack));
return 0;
}
反汇编可以看到,这里的operator new 和 malloc 执行的内容一致,都只是开了空间。没有调用构造函数。
如果我们使用new开辟空间
Stack* pst= new Stack;
可以看见,这里先调用了一次operator new,然后调用构造函数对空间进行初始化。
- 抛异常
如果在c语言中,malloc失败会返回NULL,但是 operator new 如果开空间失败,会发生抛异常。
如:
int main()
{
long* ptr = new long[0x7ffffffff];
return 0;
}
这里就会发生抛异常。会直接崩掉。
3.2. try catch
如果发生异常,我们可以使用 try catch 来捕获异常。
int main()
{
try
{
char* p1 = new char[0x7ffffffff];
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
return 0;
}
这里就能捕获抛异常的情况
捕获的位置是try里面,也就是只要try里面出现问题,就会被捕获,但是try外不会捕获。
如果try内调用函数出现问题,也可以被捕获。
4. new和delete的实现原理
- 内置类型:
如果申请的是内置类型的空间,new和malloc,delete和free基本相似
不同的地方有:
new/delete申请和释放的是单个元素的空间,
new[]和delete[]申请的是连续的空间,
而new在申请失败会抛异常,malloc会返回NULL; - 自定义类型
new的原理
- 调用operator new的函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造。
delete的原理
3. 在内存上执行析构函数,完成对象中资源的清理工作
4. 调用operator delete 函数释放对象的空间
new[N] 的原理
5. 调用operator new[]函数,在operator new[]中调用operator new函数完成N个对象空间的申请
6. 在申请的空间上执行N此构造函数
delete[]的原理
7. 在释放对象空间执行N次析构函数,,完成N个对象中资源的清理。
8. 调用operator delete[]释放空间,实际在operator deletep[]中调用operator delete来释放空间。
delete不匹配的问题:
class A
{
public:
A(int n = 0 )
:_a(n)
{}
private:
int _a;
}
上面的代码是一个A类,只自己写了构造函数,如果我们用其开辟空间:
int main()
{
A* a1 = new A[10];
delete a1;
return 0;
}
此时代码可以正常运行。
如果换成delete[]:
delete[] a1;
代码也能正常运行。
如果我们把A类改一下:
class A
{
public:
A(int a = 0)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
这里只是自己实现了析构函数,其他未变。
这里会调用10次析构函数,但是如果我们使用delete:
这里程序直接崩了。
为什么加上析构delete就会崩溃
我们先要清楚new和delete的机制
比如上面我们new了A[10]的空间,但是new[],不止会开辟这么大的空间。
这里,他会在最前面开辟4字节的空间,存储开辟的A的个数10,同时,我们需要的a1 指针会指向多开的4字节的后面,需要用到的 sizeof(A)*10 空间的前面。
那这4字节的空间又有什么用呢?
在delete的时候,它会先调用 析构函数 , 然后在调用operator delete 。
如果是 delete[] , 这个a1 指针会向前移动4字节
这里会先读取前4字节的内容,然后决定后面调用几次析构函数。
这里读取之后,就是上面的结果,会调用10 次析构函数。
但是直接使用delete
这里的 a1 指针就不会往前移动,析构函数,析构函数只会调用一次,导致后面的空间没有析构掉。
为什么delete不加析构就能运行
没有析构的情况下,会使用默认的析构函数,但是编译器自己生成的默认的析构函数,什么都没有做。
既然什么都不做,编译器这里就会优化不去调用析构函数,因此在调用operator delete的时候,默认后面所有的空间都已经经过析构的处理了,所以可以直接删除。
但是这样的话,数组最前面的那4个字节是没有被删除的。因此会造成内存泄漏
5. 定位new表达式
定位new表达式是在已分配的原始内存空间汇总调用构造函数初始化一个对象。
int main()
{
Stack* pst1 = (Stack*)operator new(sizeof(Stack));
pst1->Stack();
pst1->~Stack();
return 0;
}
这里的构造函数我们不能显式调用,析构函数就可以显式调用
如果直接这样写,程序就挂了,我们需要显式调用构造函数
new(pst1)Stack(4);
这种写法就是先开空间,然后调用构造函数初始化
Stack* pst1 = (Stak*)operator new(sizeof(Stack));
new(pst1)Stack(4);
从功能上来说,上面的操作和new两个是没有区别的。
一般情况下不会这样写,有些特殊场景会用到。有时候不想让栈在堆上创建(效率问题),就可能会使用这种方式。
6. malloc/free和new/delete区别
- malloc和free是函数,new和delete是操作符。
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需要跟上空间类型即可,如果是多个对象,[]中指定对象个数即可。
- malloc返回值是(void*),在使用时必须强制类型转换,new不需要,因为new后面是空间类型。
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数和析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。