C语言无法方便进行内存管理,C语言有关空间的所有操作都充满了冗余操作;
而C++通过new和delete操作符进行动态内存管理。
简而言之,C++对内存管理的创举主要是让我们输入的信息更高效的被编译器理解。
打个比方,C语言开辟空间就像是:你要开席,你叫C语言去买10瓶可乐,C语言会做下面这些事:
问无效信息;
它一次只买一瓶,重复十次买一瓶的操作,它每买一瓶都问你去哪家店买、走哪条路;不会思考;
买可乐的预算有多少,购买策略要你全部说清楚,购买时遇到意料之外的情况就打电话问你;轻易放弃,结果不上报;
如果C语言在去的第一家商店购买失败,它就不买了,也不会主动报告购买失败,需要我们专门询问,要是不问C语言就放任错误发生;
而C++就像是:你叫C++去买10瓶可乐,它会问你一些信息,然后再做以下事情:
主动思考
C++会问你给它多少预算,给多少人喝,每人喝几杯,购买现场它根据这些信息自己决定购买策略。灵活多变
如C++在商店购买失败,它会自动换一家商店买。有责任感
换商店会一直换到成功为止,除非彻底买不到才打电话上报。
如下代码,我们的计划实现:
为了实现以上功能,我们要写大量和我们的意图没有任何关系的内容。
代码演示:用C语言的方法开辟空间,可以看到这些代码是非常冗余的:
void Test()
{
int* p1 = (int*)malloc(sizeof(int)); //开辟一个int大小的空间
int* p2 = (int*)calloc(4, sizeof(int)); //开4个int大小的空间并且初始化为0
int* p3 = (int*)realloc(p2, sizeof(int) * 10); //扩容,把大小扩大到10个int,并且转移空间地址到p31
}
糟糕的是上面三行代码如果开空间失败不会自动报警
在实践时可以发现,我们编码时思维聚焦于开辟空间的用途;
同时也发现,实现功能时会反复使用同样的空间大小和变量类型。
如下代码,我们要修改开辟的空间属性,只需要在开空间的代码上微调即可;
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请5个int类型的空间,并初始化为0
int* ptr6 = new int[5];
// 动态申请5个int类型的空间并初始化前3个空间,后面2个空间默认为0
int* ptr7 = new int[5]={1,2,3};
delete ptr4;
delete ptr5;
delete[] ptr6;
delete[] ptr7;
}
简而言之,开辟一个空间,进行一番操作初始化它,就是我们的常做的操作;
由此,通过自定义类型就可以实现:通过一个类我们可以在开辟一个空间的同时启动一个构造函数对这个空间进行操作;
如下代码:
我们定义了一个自定义类型A,当我们使用new来开辟空间时,会自动启动A的构造函数;
#include
using namespace std;
static int i = 1;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A(" << i++ << "):" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是
// new/delete对于【自定义类型】除了开空间,还会调用构造函数和析构函数
A* p4 = new A(1); // 开辟一个int空间
A* p5 = new A[10]{1,2,3,4,5};
A* p6 = new A[10];
delete p4;
delete[] p5;
delete[] p6;
return 0;
}
开辟空间(1、2),释放空间(3、4):
new实现:
1.开辟空间、2.初始化空间;
delete实现:
3.释放空间、4.指针置空;
在申请自定义类型的空间时,new会自动调用operator new和构造函数,delete会自动调用operator delete 和 析构函数。
- 在new 起作用的过程中,固定发挥malloc、抛异常、构造函数初始化空间;
- 在delete 起作用的过程中,固定发挥free、抛异常的作用,根据情况来判断是否调用析构函数 释放空间;
如果开空间时没有malloc开辟空间,则当我们要释放空间时,我们可以去使用free或delete释放空间,因为此时delete只有free可以发挥作用,没有调用析构函数的必要;
如下代码:对p1象占用了int类型的空间,我们使用free就可以释放这个空间;
相同情况下,p2使用delete也是可以的;
同理,p3的int【10】也是free、delete皆可。
#include
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << a << "构造" << endl;
}
private:
int _a;
};
int main()
{
// new和delete内含的功能会根据具体的情况选择性发挥作用
A* p1 = new A(1); //这里p1使用了int空间,我们delete或free释放即可
A* p2 = new A(1); //这里p2使用了int空间,我们delete或free释放即可
free(p1);
delete p2;
A* p3 = new A[10];
delete[] p3;
return 0;
}
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
如下代码我们尝试通过new来创建一个对象
#include
using namespace std;
typedef char DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
cout << "Stack()" << endl;
_array = new DataType[capacity];
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
_array = nullptr;
_size = 0;
_capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
Stack* func()
{
int n;
cin >> n;
Stack* pst = new Stack(n);
return pst;
}
int main()
{
Stack* ptr = func();
ptr->Push(1);
ptr->Push(2);
delete ptr;
return 0;
}
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
A* p1 = new A(1);
delete p1;
A* p2 = new A[10];
delete[] p2;
delete[]被设计用来释放多个连续的同构造空间,那么它需要获取两个信息:
- 被释放空间的位置——在delete[]后接的地址p2;
- 调用几次析构函数——在我们使用new[]的时候,会自动在存储信息的空间前面开一个空间用来记录该空间存储了多少个对象。
如下代码:
A* p2 = new A[10];
delete[] p2;
我们使用了new A[10]开辟了十个存储int数据的空间,我们的指针p2也指向了这个空间的第一个元素的地址;
而当我们调用delete[]来释放该空间时,delete[ ]空着的[ ]就会把p2前面四个字节的内容作为整形装到[]里面,于是在编译器看来 delete[] p2 就变成了 “ delete[10] p2” ;
由此编译器知道了需要调用十次析构函数释放该空间,同时p2也被修改指向了该空间真正的起始地址,最后释放空间时会把开头的4个字节和后面的40个字节一起释放掉,然后p2被置空。至此new A[10]所占用的空间被全部释放;
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type
或者
new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
即,有些情况下编译器会使用内存池来优化运行效率,而这时我们要开辟空间时就会出现空间开辟到内存池上而不是堆上,如此一来开辟空间就相当于失败了,因为内存池不能像堆一样一直保存信息;
所以有了placement-new的概念,专门在那些使用内存池的编译器上发挥作用,特别地要求开辟空间要在堆上开辟而不是内存池上。