【C++】内存管理
目录
一、C/C++内存分布
二、C语言中动态内存管理
三、C++中动态内存管理
1、new/delete操作内置类型
2、new/delete操作自定义类型
四、operator new与operator delete函数
五、new和delete的实现原理
1、内置类型
2、自定义类型
六、定位new表达式(placement-new)
七、malloc/free和new/delete的区别
一、C/C++内存分布
我们知道,不同类型的变量是被存放在不同的内存空间中的,那么哪些变量会被存放在哪种内存空间中呢?我们先来看下面这段代码:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里? __C__ | staticGlobalVar在哪里?__C__ |
staticVar在哪里? __C__ | localVar在哪里? __A__ |
num1 在哪里? __A__ | |
char2在哪里? __A__ | *char2在哪里? __A _ |
pChar3在哪里? __A__ | *pChar3在哪里? __D__ |
ptr1在哪里? __A__ | *ptr1在哪里? __B__ |
其中容易弄混的是 *char2 与 *pChar3 。
对于数组 char2 ,字符串数组"abcd" 是被存放在常量区的,但是编译器在栈区中为数组 char2 开辟了一块空间,并把 "abcd" 拷贝给了数组 char2 ,因此 char2 与 "abcd" 是两块不同的空间,*char2 在栈区。
对于指针 pChar3 ,它是指向存放在常量区的字符串数组 "abcd" 的,因此 *pChar3 在常量区。
二、C语言中动态内存管理
C语言中开辟空间的方式有: malloc 、 calloc 、 realloc 。
我们自己指定开辟的空间大小,通过函数调用返回一个 void* 类型的指针,再把该指针强转成指定类型。
其中 malloc 与 calloc 的区别在于使用 calloc 开辟空间时会进行初始化,而 malloc 不会。
realloc 用于对已有的空间进行扩容。扩容又分为原地扩容与异地扩容。
原地扩容:如果已有空间后有足够的连续空间,就在已有空间后直接进行扩容。
异地扩容:如果已有空间后没有足够的连续空间,就在其他地方重新开辟一块空间,并把原有空间的内容拷贝到新空间内,释放原有空间。
原地扩容:
对 p2 指向的空间进行扩容,扩大到 10 个整型大小。由于原有空间后的空间足够大,就直接在原地进行扩容。此时, p2 与 p3 指向同一块空间,因此在释放空间的时候只需要释放一次就可以了。
异地扩容:
对 p2 指向的空间进行扩容,扩大到 100 个整型大小。由于原有空间后连续的空间大小不够,就需要在异地重新开辟空间。并把原有的空间释放掉。此时, p2 与 p3 依然指向同一块空间(即新空间),因此在释放空间的时候只需要释放一次就可以了。
三、C++中动态内存管理
由于C++兼容C语言,所以C语言中适用的方法在C++中依然适用。除此之外,C++又提出了一些自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
1、new/delete操作内置类型
在C++中,开辟空间不再使用函数调用,而是使用一个操作符 new 。
意为 new 一个 int 对象,并把这个对象交给 p2 指针,这里不需要强制类型转换。
如果我们想要开辟多个对象,可以写成如下形式:
把对象个数用方括号括起来。
在功能上来说,C语言的写法和C++的写法效果是一样的。与 malloc 相同, new 出来的对象默认同样没有进行初始化。
如果想在 new 出一个对象的同时,对他进行初始化,则可以采取以下写法:
把初始化内容使用小括号括起。
这里要注意小括号与中括号的区别,如果我们想要申请 10 个 int 的数组,则可以这样写:
如果要释放空间(对象),则使用另外一个操作符 delete 。
连续释放多个对象,就在 delete 后加上方括号。
各个操作符、符号、数字的意义如图所示:
注意:申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用
new[] 和 delete[] 。
2、new/delete操作自定义类型
在以上的用法中, new 只起到了一个简化代码的作用,而 new 真正特殊的用途体现在自定义类型上。 new / delete 和 malloc / free 最大区别是 new / delete 对于自定义类型除了开空间之外还会调用他的构造函数和析构函数。
注意:在申请自定义类型的空间时, new 会调用构造函数, delete 会调用析构函数,而 malloc 与
free 不会。
通过这个性质,我们再来编写链表等数据结构的代码时,就会变得非常方便:
每 new / delete 一个链表节点,就会调用一次构造函数 / 析构函数,不需要自己来进行初始化设置以及释放空间。所以说 new / delete 是为了自定义类型而诞生的。
注意:
- 使用 new 开辟的空间,要用 delete 来释放
- 使用 new[] 开辟的空间,要用 delete[] 来释放
- 使用 malloc/calloc/realloc 开辟的空间,要用 free 来释放
要严格匹配使用,不要交叉,否则结果是不确定的
四、operator new与operator delete函数
operator new 和 operator delete 是系统提供的全局函数,虽然函数名字中带有 operator ,但是和运算符重载没有任何关系。
我们先来看一下 operator new 和 operator delete 的源码:
operator new 的底层是通过 malloc 来实现的,不同之处在于如果空间申请失败会进行抛异常,例如:
使用 operator new 和 malloc 来开辟空间的方法看起来很相似。区别在于 p1 空间开辟失败时,会抛异常,而 p2 空间开辟失败时,通过返回空指针,并进行判断来结束程序。
operator delete 的底层是经过一系列的检查(比如越界检查等等)之后,调用 _free_dbg 来释放空间。而我们经常使用的 free 其实也是一个调用 _free_dbg 的宏函数。因此 operator delete 的底层是通过 free 来释放空间的。
综上,我们可以理解为 operator new 和 operator delete 本质上是 malloc 和 free 的封装。C++封装malloc 和 free 的原因是C++是一门面向对象的语言,而面向对象的语言在处理错误时,使用的方法都是抛异常。
五、new和delete的实现原理
1、内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常,malloc 会返回 NULL。
2、自定义类型
- new的原理
1. 调用operator new函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造- delete的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间- new T[N]的原理
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
2. 在申请的空间上执行N次构造函数- delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
实际上我们使用的 new 底层是使用 operator new 来实现的,而 operator new 是 malloc 的封装。
delete 同理,底层是使用 operator delete 来实现的,而 operator delete 是 free 的封装。在释放 p1 指向的空间之前,先调用析构函数。
这样就可以解释为什么 new 与 delete、new[] 与 delete[] 、malloc/calloc/realloc 与 free 要分别匹配使用了。
这是因为 free 不会调用析构函数,如果被 free 释放的空间里的自定义类型中有资源没有被清理,就会造成内存泄漏。如果被 free 释放的空间里的自定义类型中没有资源需要清理,则不会有任何问题。
比如以下代码:
因为 st 是自定义类型变量,所以会自动调用构造函数与析构函数。
而当我们定义了一个 Stack 类型的指针变量时,因为指针是内置类型变量,所以出作用域时不会自动调用析构函数,导致内存泄漏。
因此我们要自己在下面写上释放内存的代码:
但是如果我们在释放内存时,使用的是 free 而不是 delete ,就会导致被释放的空间里的栈类型中还有占据四个 int 的空间资源没有被释放,导致内存泄漏。在C/C++中,内存泄漏是不会报错的,所以一些隐形的内存泄漏会存在相当大的隐患。
结论:new/malloc系列,有底层实现机制有关联交叉,不匹配使用可能有问题,也可能没问题,建议大家匹配使用。
还有一些情况下,如果不匹配使用,编译器会直接报错:
1、
2、
当我们使用 new[] 来开辟空间时,不论使用 free 还是 delete 来释放空间都会报错。只有使用 delete[] 才不会有问题。这与编译器的实现机制有关系。
我们使用 new[] 来开辟空间时,编译器通过 [] 中的数字得知需要调用多少次构造函数,但是当我们使用 delete[] 释放空间时,编译器怎么知道调用多少次析构函数呢?
为了解决这个问题,编译器采取了以下措施,我们以上面的代码为例,因为调用了 10 次析构函数,一个 A 是 4 个字节大小,十个 A 就是 40 个字节大小,但是编译器会在该 40 个字节的空间前面另外多开 4 个字节大小的空间,用于记录调用构造函数的次数,共给 delete[] 读取需要调用析构函数的次数。
也就是说编译器在申请空间的时候是从最前面的红色标注的位置开始申请的,但是返回时,返回的是 p1 指针。
因此,我们在使用 free、delete 时会报错,是因为我们释放空间的位置就是错的,不应该从 p1 开始释放空间,而应该在最前面红色标注的空间的地方开始。而 delete[] 会自动把 p1 往前减 4 个字节,从而读取需要调用析构函数的次数,再依次调用析构函数,调用完毕后,从 p1 往前减 4 个字节的位置把空间释放掉。
现在我们把类类型的析构函数注释掉再来看一下:
程序运行成功,因为我们没有写析构函数,所以编译器就没有在前面多申请 4 个字节的空间,返回的 p1 就是整个空间最开始的位置。
六、定位new表达式(placement-new)
如果想要对一块已有的空间进行初始化,则可以使用定位new。
把 p1 指向的空间进行初始化,这里简单了解就好,以后会详细讲解。
如果想要释放这块已有空间,则可以直接显示调用析构函数:
看到这里,可能同学们会有疑问,以上的所有操作,明明可以直接使用 new 与 delete 来直接实现,何必要绕这么一大圈呢?直接写成以下形式不就好了吗?
int main()
{A* p1 = new A(1);delete p1;return 0;
}
在这种情况下,的确是这样写起来更加简单快捷。但是有些时候,如果用 new 在操作系统的堆中申请内存,会比较慢。所以为了追求性能,我们都是从内存池中申请内存的,此时,就需要对内存池中的已有空间进行初始化了,关于内存池的相关知识,以后会做详细讲解。
七、malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地
方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
关于C++内存管理的内容就讲到这里,希望同学们多多支持,如果有不对的地方,欢迎大佬指正,谢谢!