C++中的内存管理
✨前言✨
📘 博客主页:to Keep博客主页
🙆欢迎关注,👍点赞,📝留言评论
⏳首发时间:2023年11月21日
📨 博主码云地址:博主码云地址
📕参考书籍:《C++ Primer》《C++编程规范》
📢编程练习:牛客网+力扣网
由于博主目前也是处于一个学习的状态,如有讲的不对的地方,请一定联系我予以改正!!!
C++内存管理
- 1 C++内存分布
- 2 C++内存管理方式
- 2.1 处理内置类型
- 2.2 处理自定义类型
- 3 operator new与operator delete函数
- 4 定位new表达式
- 5 malloc/free和new/delete的区别
1 C++内存分布
C++根据不同的数据需求有着不同的存储特性,所以将内存分为多个区域来存储数据!
2 C++内存管理方式
在C语言中,我们是利用malloc/calloc/relloc进行动态内存管理!其中relloc就是对之前malloc申请的空间进行扩展,calloc就是malloc加上memset(),而C++是通过new和delete操作符进行动态内存管理!
2.1 处理内置类型
利用new和delete操作符进行动态内存管理比C语言中的malloc方便好写了许多!
int main()
{
int* b1 = new int; //申请一个int的空间
int* b2 = new int[5]; //利用new[]操作符,申请5个连续int的空间,就是一个数组
delete b1;
delete[] b2;
return 0;
}
通过调试,我们可以进一步的了解new和delete操作符是如何处理内置类型的!
可以发现,new其实和C语言中的malloc是一样的,也是不会进行初始化的,只是动态开辟空间!相比较于malloc而言,new是不用进行强制类型转换,也不用自己去计算要开辟空间的大小了!同理,delete就相当于free,释放对应开辟的空间!
注意:new和delete要搭配使用,new[]和delete[]搭配使用!不要混着使用,否则会出现内存泄露或者程序不能正常运行的问题!
利用new其实也是可以对内置类型进行初始化的!代码如下:
int main()
{
int* p1 = new int(10); //开辟一个int空间,结合()中的内容进行初始化
int* p2 = new int[5] {1, 2, 3}; //开辟5个连续的int空间,结合{}中的内容进行初始化
delete p1;
delete[] p2;
return 0;
}
调试结果如图所示:
可以发现对于5个连续的int空间,只要我们给定一些空间的初始化,后面的空间编译器就把它初始化成为0的!
2.2 处理自定义类型
对于自定义类型如果使用malloc,我们就无法对自定义类型进行初始化!因为构造函数不可以通过所给的对象指针直接调用:
利用new和delete操作符如何处理自定义类型呢?我们可以参考如下代码进行理解:
class Date {
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2023 , int month = 11, int day = 21)
{
cout << "Date 构造函数调用了" << endl;
_year = year;
_month = month;
_day = day;
}
~Date(){
cout << "析构函数被调用了" << endl;
}
};
int main()
{
Date* d1 = new Date(2023,11,21);
Date* d2 = new Date[5]{ Date(2023,11,20) };//初始化一部分,其他的是默认构造初始化,如果没有默认构造也会报错
Date* d3 = new Date;//调用默认构造,无默认构造就会报错
delete d1;
delete[] d2;
delete d3;
return 0;
}
运行结果如下:
从以上我们就可以知道new和delete处理自定义类型的原理
new就是开辟对象空间+调用构造函数
,malloc就不会调用构造函数
delete就是调用析构函数+释放对象空间
,free就不会调用析构函数
3 operator new与operator delete函数
operator new和operator delete是系统提供的全局函数!我们先来看这样一段代码:
class Stack {
private:
int* p;
int _capacity;
int _top;
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
p = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] p;
_capacity = _top = 0;
p = nullptr;
}
};
int main()
{
Stack st1;
Stack* st2 = new Stack;
return 0;
}
如何理解上述的两个new呢?如图所示:
第一个new是开辟对象的空间,第二个new是开辟对象中p所指的空间!这也就和之前的知识点联系上了,利用delete就是先调用析构,清理对象中的额外资源也就是动态开辟所需的空间!在释放对象的空间!通过这个例子,我们可以更深刻的理解new和delete的工作原理!那么new和delete底层是怎样实现的呢?
运行以下这段代码:
int main()
{
Stack* st1 = new Stack;
Stack* st2 = (Stack*)operator new(sizeof(Stack));
return 0;
}
调试与运行结果如图:
我们可以发现通过operator new函数和malloc一样,只是开辟了空间,并不会调用构造函数进行初始化!实际上operator new底层封装的就是malloc!那为什么不直接用malloc呢?那是因为我们要面向对象编程,malloc返回值是0,我们通过operator new是可以抛异常的!更加符合面向对象编程!而operator delete也同理,是封装了free!简单来说既可以用下图来概括!
4 定位new表达式
可以通过定位new显式的调用构造函数!在实际应用中,定位new一般是配合内存池使用的,因为内存池分配出来的空间没有初始化,因此如果需要在这块内存池分配出来的空间上构造自定义类型的对象,需要使用定位new
显式调用构造函数构造目标对象
格式如下:
new(申请对象地址)类型
new(申请对象地址)类型(类型的初始化列表)
例子如下:
//定位new的使用
class A {
private:
int _a;
int _b;
public:
A(int a = 10)
{
cout <<"A(int a = 10)"<< endl;
_a = a;
}
A(int a , int b )
{
cout << "A(int a = 1, int b = 2)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A* a1 = (A*)operator new(sizeof(A));//开辟对象的空间
new(a1)A;//定位new,显式的调用构造函数,调用默认构造
a1->~A();//显式的调用析构函数
free(a1);//释放对象空间
A* a2 = (A*)operator new(sizeof(A));
new(a2)A(10, 20);//定位new,调用两个参数的构造函数
a2->~A();
free(a2);
}
显式调用了构造函数,那就要显式调用析构函数,要配对使用就可以了!
5 malloc/free和new/delete的区别
相同点:都是从堆上开辟空间,都需要手动的进行释放!
不同之处在于:
1️⃣malloc和free是函数,而new和delete是操作符。
2️⃣malloc不会进行初始化,new开辟空间的同时可以初始化。
3️⃣malloc开辟失败是返回NULL需要我们进行判断,new开辟失败是抛出异常。
4️⃣malloc开辟空间需要手动计算大小并进行传递,而new就可以不用去计算大小!
5️⃣对于自定义类型,malloc不会去调用构造函数,free不会去调用析构函数!而new会去调用构造函数进行初始化,delete会去调用析构函数进行清理!