C++内存管理
1.c/c++内存分布
c/c++的内存划分如下图
系统为什么要将内存区域划分成不同的区域?
因为不同的数据,有不同的存储需求,各区域满足不同的需求
在这里我们简单说明一下
1.栈又叫堆栈—非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2.内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享共享内存,做进程间通信。
3.堆用于程序运行时动态内存分配,堆是可以向上增长的。
4.数据段—存储全局数据和静态数据。(静态区)
5.代码段—可执行的代码/只读常量。(常量区)(这里的可执行代码不是我们写的代码,而是进行汇编转换成的二进制指令)(而我们平时所写的代码存放在磁盘中,它的本质就是一个文件)
临时变量/函数 存放于 栈帧
动态使用(需要时调用不需要时释放(堆区))
(在数据结构中使用频率较高,算法中需要动态开辟空间(如:归并排序),)
整个程序期间长期使用
只读数据 (常量,可执行代码)
在了解c/c++内存分布之前我们先看一下下面这段代码
int global_val = 1;
static int static_global_val = 1;
void Test()
{
static int static_val = 1;
int local_val = 1;
int num[10] = { 1, 2, 3, 4 };
char p_char2[] = "abcd";
const char* p_char2 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int)* 4);
free(ptr1);
}
我们来看一些问题
1.选择题
选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
初阶
global_val在哪里? _____ , static_global_val在哪里?_____
static_val在哪里?_____ , local_val在哪里?____
num在哪里?____
进阶
char1在哪里? ____ , *char1在哪里?____
p_char2在哪里?____ , *p_char2在哪里?____
ptr1在哪里?____ , *ptr1在哪里?____
来看看你的答案是否正确
1.选择题
选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
global_val在哪里? C , static_global_val在哪里?C
static_val在哪里?C , local_val在哪里?A
num在哪里?Achar1在哪里? A , *char1在哪里?A
p_char2在哪里?A , *p_char2在哪里?D
ptr1在哪里?A , *ptr1在哪里?B
在上述代码中
globla_val 与 static_global_val 都在数据段(静态区)
哪它们有什么区别呢?
他们最本质的区别就是链接属性
前者在所有文件可用
后者在当前文件可用
那么在上述代码中两个静态变量又有什么区别呢?
前者在当前文件可以使用
后者在当前函数可以使用
但是上述的三个变量它们的生命周期都是一样的在整个函数期间都可以使用
2.C语言中动态内存管理方式
malloc/calloc/reallloc/free;
c++中动态内存管理
在C++里面,C语言的内存管理方式可以继续使用,但有些地方不方便,因此C++提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理;
void Test()
{
//动态申请一个int类型的空间
int* prt1 = new int;
//动态申请一个int类型的空间并初始化为10
int* prt2 = new int(10);
//动态申请10个int类型的空间
int* prt3 = new int[10];
//动态申请多个空间并初始化
int* prt4 = new int[3]{1,2,3};
delete prt1;
delete prt2;
delete[] prt3;
delete[] prt4;
}
首先我们要明确一点,new和delete是操作符,不是函数;
这里的new和我们之前学到的malloc在除用法上没有任何区别
但是我们在使用malloc开辟一个空间prt1后无法对A类进行初始化,也无法通过构造函数进行初始化,我们之前学过,我们在初始化类时,类自己会自动调用构造函数,但是在这里,我们无法对A类中的私有变量_a 进行初始化,通过上述代码我们可以清楚的了解1.成员"A::_a"不可访问 2.类型名称“A”不能出现在类成员访问表达式的右侧。所以说malloc不方便解决动态申请自定义类型对象的初始化问题,但对于内置类型 malloc和new在除用法上没有其他区别。
在这里我们可以看到new可以对自定义类型进行初始化,在这里new干了两件事1.开辟空间 2.调用构造函数
当我们不想调用默认构造,我们可以直接显式调用(如ptr3)
到这里就会有人问,为什么malloc不调用构造函数,这里我们就要明确下malloc是属于C语言库里面的函数,不能调用构造函数。我们学习C++时,我们了解到C++兼容C语言。但是C语言库中的函数malloc,不能使用C++的构造函数。
所以说**C++的new解决了对自定义类型动态开辟完空间后的初始化。
我们之前实现的单链表就可以使用上述代码完成单链表的动态空间开辟以及初始化。
如果我们同时开辟多个空间,那我们应该如何进行初始化。
其实new的本质就是开空间+调用构造函数初始化 。
在上面的内容中我们了解了new下面我们来看一下delete
delete的本质就是调用析构函数+释放空间。
delete在使用时与new相匹配。
我们了解当malloc开辟空间失败时会返回NULL,那么new呢?
在这里new开辟空间失败会抛异常
那么什么情况下new会失败了?我们使用new开辟较大的空间,或者没有内存了
我们演示一下
执行上述左侧代码,我们会发现一直打印"屯"
在这里我们new失败了会直接跳到catch不会执行try的后续代码,因此new不需要检测返回值;面向对象一般失败都会抛异常,不会以返回值的形式判断失败;
小点总结:
1.在内置类型的对象申请释放,new和malloc除了用法上 ,没有区别。
2.new的本质就是:开空间+调用构造函数初始化
3.delete的本质就是:调用析构函数+释放空间。
operator new 与 operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和 operator delete是系统提供的全局函数,new在底层调用 operator new全局函数来申请空间,delete在底层通过operator delete全局函数来说释放空间。
new动态申请内置类型
new动态申请自定义类型
通过反汇编我们发现无论是内置类型还是自定义类型,在使用new动态开辟的时候,都会调用 operator new,那么operator new是什么呢?
在这里operator new 是库里面的全局函数,封装了malloc,我们来看一下它的底层实现
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
//try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
在这里我们可以看到new的底层是用malloc来实现的,但是与malloc不同的是,malloc动态申请失败后会返回NULL,但是通过函数封装malloc来实现的但是动态申请失败后会抛异常,符合C++机制,失败抛异常!这也就是为什么要封装malloc的原因。
这里delete也是和new是一样的他的底层是free来实现的。 它与new配对使用
但是当我们查看反汇编的时候 发现只调用了析构函数,到底有没有调用operator delete呢?
当我们进入他的析构反汇编我们发现它先调用了析构函数然后调用了operator delete。
new 和 delete 的实现原理
内置类型:
在上文中我们说到当我们申请的是内置类型的空间的时候,new和malloc , delete 和 free基本类似。
不同点是new申请的是单个元素的空间, 而 new[ ] 申请的是连续空间,而且new申请空间失败后会抛异常,malloc则会返回 NULL。
对于自定义类型:
new的实现原理
第一步,调用operator new 函数申请空间。
第二步,在第一步所申请的空间上执行构造函数,实现对象资源的初始化工作。
申请空间+初始化
delete 的实现原理
第一步在空间上执行析构函数,实现对象资源的清理工作。
第二步调用operator delete函数释放对象的空间
资源清理+空间释放
new T[N]的原理
第一步调用operator new[ ] 函数,在operator new[ ] 中实际调用 N 次operator new 完成对 N 个对象空间的申请
第二步 在第一步申请的空间中执行 N 次构造函数对其初始化。
delete[ ]的原理
第一步,在申请的空间上执行 N 次析构函数,完成对空间中 N 个对象的资源清理。
第二步,调用operator delete[ ] 释放空间,其实质就是在operator delete[ ] 中调用 operator delete释放空间
定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new(place_address)type或者new(place_address)type(initializer-list)
place_address必须是一个指针,initialize-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以说如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
//定位new/replacement new
int main()
{
//p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数还没有执行
A* p1 = (A*)malloc(sizeof(A));
new (p1)A; //注意:如果A类的构造函数有参数时,此处需要传参。
p1->~A;
free(p1);
A* p2 = (A*)operator new (sizeof(A));
new (p2)A(10);
p2->~A;
operator delete(p2);
return 0;
}
定位new的应用场景:定位new表达式在实际中一般是配合内存池使用。
有些场景我们需要提高效率会在内存池开空间,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
常见面试题
malloc/free 和 new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
1.malloc和free都是函数 , new和delete是操作符
2.malloc申请的空间不会初始化,new可以初始化
3.malloc申请空间时,需要手动计算空间大小并传递,new只需要在其后跟上空间的类型即可,如果是多个对象,[]中指定多个对象即可。
4.malloc的返回值为void* ,在使用时必须强转,new不需要,因为new后跟的是空间的类型
5.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6.申请自定义类型对象时,malloc/free只会开辟和释放空间,不会调用构造函数和析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中的资源清理。