c/c++-memory-management
C/C++内存管理
1. 内存分布
-
栈:系统自动管理,通常存储函数局部变量。
-
栈是向下增长的,大小通常几MB
-
申请释放速度快
-
-
堆:手动申请和释放。
-
堆是向上增长的,可用空间大
-
申请释放速度慢
-
-
数据段:存储全局变量和静态变量。
- 全局数据又分为已初始化和未初始化
-
代码段:存储正文代码和常量。
- 代码段通常是只读的
-
共享区:通常是多个进程可以共同访问的内存区域。
-
进程间通信,允许多个进程读写同一块内存,高效传递数据
-
动态库的加载,动态库在内存只需加载一次,多个进程可以共享
-
2. C
函数原型 | 作用 |
---|---|
void *malloc( size_t size ); | 分配连续的指定字节数的未初始化内存 |
void *calloc( size_t num, size_t size ); | 分配 num 个大小为 size 的连续内存空间,并初始化为0 |
void *realloc( void *memblock, size_t new_size ); | 调整之前分配内存块的大小,new_size 新的字节数 |
void free( void *memblock ); | 释放之前分配的内存 |
-
函数统一头文件:
#include <stdlib.h>
。 -
malloc
- 分配失败,返回
NULL
。
- 分配失败,返回
-
calloc
-
malloc
开辟的空间存放随机值,而calloc
开辟后空间初始化为0
。 -
分配失败,返回
NULL
。
-
-
realloc
-
分配失败,返回
NULL
。 -
原地扩容:新空间比原空间大,原空间后续的空间足够新空间的大小,则直接在原空间的尾部进行扩容,返回原空间的起始地址。
-
异地扩容:新空间比原空间大,原空间后续的空间不够新空间的大小,
realloc
则会找一个足够新空间大小的内存空间进行分配,并将原空间的数据拷贝过来,释放原空间,返回新空间的起始地址。 -
realloc
第一个参数传NULL
,则等价malloc
。
-
-
free
-
释放之前分配的内存。
-
常见的动态内存错误。
-
对
NULL
解引用 -
非动态开辟内存使用
free
释放 -
free
释放不完整的堆内存 -
同一块内存多次释放
-
-
3. C++
在C++中,由于引入了类,继续使用C语言风格的malloc
/free
内存管理机制存在 根本性的缺陷。 主要体现在对于自定义类型,无法调用构造和析构函数。因此 C++通过 new
和 *delete
运算符进行动态内存管理。
3.1 new与delete运算符的使用
3.1.1 动态申请
// 动态申请
int* ptr = new int;
ClassName* ptr = new ClassName; // 调用默认构造函数
3.1.2 动态申请并初始化
// 动态申请并初始化
int* ptr = new int(1);
ClassName* ptr = new ClassName(arg1, arg2); // 调用带参构造函数
3.1.3 释放变量/对象
// 释放对象
delete ptr;
3.1.4 动态申请数组
// 动态申请数组
int* ptr = new int[3];
ClassName* ptr = new ClassName[3];
3.1.5 动态申请数组并初始化
// 动态申请数组并初始化
int* ptr = new int[3]{1 , 2 , 3};
ClassName* ptr = new ClassName[3]{ClassName() , ClassName() , ClassName()};
3.1.6 释放数组
// 释放数组
delete[] ptr;
申请和释放自定义类型(对象)时,new会调用构造函数,delete会调用析构函数。
3.2 operator new与operator delete函数
operator new
和 operator delete
是系统提供的全局运算符重载函数。
-
operator new 函数实际通过 malloc 申请空间,但是当申请空间失败时,会 抛异常。
-
operator delete 函数实际通过 free 释放空间。
3.3 new和delete的实现原理
如果申请的是内置类型,new和malloc,delete和free基本类似,而申请对象是自定义类型时,new和delete会自动调用构造函数和析构函数。
3.3.1 new原理
-
调用 operator new 函数申请空间。
-
在申请的空间上调用该对象的构造函数。
3.3.2 delete原理
-
调用该对象的析构函数。
-
调用 operator delete 函数释放对象空间。
3.3.3 new T[N]原理
-
new 调用 operator new[],operator new[] 中实际调用 operator new 函数完成对N个对象申请空间。
-
在申请的空间上执行N次构造函数。
3.3.4 delete[] 原理
-
在释放的空间上执行N次析构函数。
-
delete 调用 operator delete[],operator delete[] 中实际调用 operator delete 函数完成对N个对象释放空间。
3.4 delete对new []是为定义行为?
先来看一段代码:以下代码 delete aptr
程序崩溃了,而 delete bptr
程序没有崩溃,这是为什么?
// vs 2019
#include <iostream>
using namespace std;class A {
public:~A() {cout << "aaa" << endl;}int a;
};class B {
public:int b;
};int main() {// 崩溃//A* aPtr = new A[3];//delete aPtr;// 正常运行B* bPtr = new B[3];delete bPtr;return 0;
}
分析malloc和new底层原理
-
malloc
它的底层实现会在分配的内存块前添加一个管理用的头信息(通常称为malloc_header
)。-
内存块的大小(这也就是为什么调用free时,不用传递空间大小)。
- 调用 free 时,通过
ptr - sizeof(malloc_header)
释放完整的内存
- 调用 free 时,通过
-
其他信息。
-
-
new
的底层也是调用malloc
开辟空间的,而new
也会记录自己管理用的头信息,尤其是new []
。-
new [] 添加
size_t N
记录析构的次数。 -
其他信息。
-
-
new
(尤其是new[]
)不会在malloc_header
前面插入数据,而是在malloc
返回的可用内存空间的开头部分存储自己的信息 (sizeof(T) * N + 额外空间
)。
-
new T[N]
(数组,T
需析构),添加size_t N
头信息。new T[N]
(数组,T
无需析构),优化为无头信息(直接malloc
)。- 当没有显示写析构函数,编译器可能会直接优化掉。
-
new T[N] 如果 T 需要析构:
-
delete ptr
会错误地认为ptr
指向单个对象,而非数组,忽略了size_t
头信息,导致 delete 底层 free 时释放不完整的空间(而且只会调用一次析构函数)。-
实际申请:malloc_header + size_t + ptr(用户实际空间)
-
误以为释放:ptr - malloc_header
-
正确释放:ptr - size_t - malloc_header
-
-
-
new T[N] 如果 T 无需析构:
delete ptr
可能“侥幸”底层调用 free 释放成功(因无头信息),但仍是 未定义行为。
严格匹配
new
/delete
和new[]
/delete[]
。
3.5 定位new表达式(placement-new)
placement-new
表达式是在 已分配的内存空间中调用构造函数初始化一个对象,核心作用是 将内存分配和对象构造分离。
-
普通
new
:分配内存 + 调用构造函数。 -
placement-new
:仅调用构造函数,内存由外部管理。 -
new(place_address)type(arg…)
-
place_address:已分配的内存指针
-
type:类型
-
arg:构造参数
-
A* ptr = (A*)operator new(sizeof(A)); // 开辟内存但是没有调用构造函数
new(p2)A(10); // placement-new调用构造函数
p2->~A(); // 显式调用析构函数
operator delete(p2); // 释放内存
4. malloc/free与new/delete的区别
malloc/free | new/delete | |
---|---|---|
1 | malloc/free是函数 | new/delete是运算符 |
2 | malloc申请的空间不会初始化 | new可以初始化 |
3 | malloc申请空间需要手动计算大小 | new T[]中指定个数即可 |
4 | malloc返回void*,需要类型转换 | new指定类型,无需类型转换 |
5 | malloc失败返回NULL | new失败抛异常 |
6 | 对于自定义类型,malloc/free不会调用构造和析构函数 | new/delete申请/释放空间,会调用构造和析构函数 |