C++ 中的 malloc、free 和 new、delete
总结一下C++的malloc、free 和 new、delete这些关键字常见的问题
views
| comments
在C++中,malloc
、new
、free
、delete
经常放在一起对比,这里以问答的形式来看看这些关键字的一些细节。
malloc、free和new、delete有什么区别?
- 背景:
malloc
和free
是C语言标准函数库中的库函数,是内存分配器,对分配的内存一无所知,只负责申请和释放一块指定大小的原始内存。new
和delete
是C++的操作符,不仅是内存分配器,还是C++对象生命周期的管理者。
- 使用方式:
malloc
需要手动计算大小,必须明确指定要申请的字节数,通常借助sizeof
来计算,成功时会返回一个void*
指针,我们必须手动进行类型强转以匹配目标类型,如果失败会返回NULL
,因此我们需要显式地检查返回值来判断是否成功。
cppint *p_int = NULL; // 1. 分配内存:需要手动计算大小,即 sizeof(int) p_int = (int*)malloc(sizeof(int)); // 2. 检查分配是否成功 if (p_int == NULL) { printf("内存分配失败!\n"); return 1; // 返回错误码 }
free
只需要指定刚刚返回的指针即可,不需要指定大小。下面是一个分配数组的例子:
cppfree(p_int); p_int = NULL;
cppint *arr = NULL; int n = 5; // 1. 为一个包含 n 个整数的数组分配内存 arr = (int*)malloc(n * sizeof(int)); // 2. 检查分配是否成功 if (arr == NULL) { printf("数组内存分配失败!\n"); return 1; } printf("为 %d 个整数分配了内存。\n", n); // 3. 使用内存 // 4. 释放整个数组的内存 free(arr); printf("数组内存已释放。\n"); arr = NULL;
new
在使用时直接指定要分配的数据类型即可,编译器会自动计算所需的大小,成功时会返回一个对应类型的指针,无需任何转换,如果分配失败则会默认抛出std::bad_alloc
异常,我们可以使用try-catch
去捕捉异常。
cppint* p_int1 = new int;//*p_int1 的值是未定义的 int* p_int2 = new int();//申请内存,并进行初始化 int* p_int3 = new int(123);//申请内存,并指定一个具体的初始值
new
申请的内存,我们需要用delete
去释放,只需指定变量名即可:我们可以使用
cppdelete p_int1; delete p_int2; delete p_int3; delete p_double;
new[]
去申请一个数组:使用
cppint* p_array1 = new int[SIZE];// 默认情况下,数组中的元素是未初始化的。 int* p_array2 = new int[SIZE]{};//可以使用花括号 {} 对数组进行值初始化(所有元素为 0)
new[]
申请的数组需要使用delete[]
去进行释放:
cppdelete[] p_array1; delete[] p_array2;
- 内存处理的差别:
malloc
分配的内存是未初始化的,它仅仅是一块原始的、内容不确定的内存空间。new
分配的内存是初始化的。对于基本数据类型,初始化的行为取决于new
的使用形式,例如new int()
会进行值初始化为0,而new int
则不保证。对于类类型对象,new
会在分配内存后自动调用该类的构造函数,完成对象的初始化工作。
- 可重载性:
malloc
和free
作为C库函数,通常是不可重载的。new
和delete
作为C++的操作符,是可以进行重载的,所以我们可以很方便地定制内存分配策略,比如使用内存池。
malloc、free和new、delete的主要调用步骤是怎样的?
malloc
:仅仅分配指定字节大小的内存块,不会调用任何对象的构造函数,因此对象的状态是未定义的。free
:仅仅释放使用malloc
分配的内存,但是不会调用对象的析构函数,如果对象持有其他资源,例如打开的文件会导致资源泄漏。new
:首先调用operator new()
函数来分配一块足够大的原始内存,在成功分配的内存之上,自动调用对应类的构造函数,完成对象的初始化。delete
:首先调用对象的析构函数执行清理工作,例如释放对象内部持有的其他资源,然后调用operator delete()
函数来释放之前分配的内存
malloc和new分配的内存来自哪里?底层的实现有何关联?
- 内存来源:通常情况下,
malloc
和new
都是在进程的堆 (Heap) 上分配内存。在C++的语境中,这块区域更准确地被称为自由存储区 (Free Store)。 - 底层实现:
new
和delete
的底层实现通常会依赖malloc
和free
,但这并非语言标准所强制要求的。具体实现取决于编译器和其附带的标准库。一个常见的实现方式是,operator new()
内部调用malloc()
来获取原始内存,而operator delete()
内部调用free()
来归还内存。C++17 后很多内存分配器(如 tcmalloc、jemalloc)可以完全脱离malloc
和free
,直接调用更高效的系统接口。
使用malloc分配内存时,内部具体发生了什么?
当调用malloc
时:
malloc
内部维护着一个或多个空闲内存块的列表,当收到一个内存请求时,会先检查这些列表,尝试找到一个大小合适的内存块。- 如果找到一个足够大的内存满足请求,
malloc
将这块内存标记为已使用,同时会进行一些元数据的记录,比如块大小,然后会返回一个指向这块虚拟内存区域的指针给程序。这个时候可能尚未发生实际的物理内存分配。 - 如果
malloc
在其管理的内存中找不到合适的空闲块,它会向操作系统请求更多的虚拟地址空间,这一般是通过系统调用来完成的,然后操作系统进程会扩展进程的虚拟内存空间。 - 即使操作系统为进程扩展了虚拟地址空间,也不一定分配相应的物理内存页,许多现代操作系统采用惰性分配和按需分页的策略。物理内存页只有进程首次尝试访问某个虚拟内存地址时,才会由操作系统通过缺页中断机制,实际分配一个物理页,并建立虚拟内存到物理地址的映射关系表。这种机制允许操作系统更有效管理物理内存,例如一个进程可能申请了大量虚拟内存,但是只使用了一小部分,这样就不需要为未使用部分浪费物理内存。
malloc主要涉及哪些系统调用?
malloc
为了平衡性能和内存碎片,通常会根据请求内存的大小选择不同的系统调用来与操作系统交互:
brk
/sbrk
:用于改变数据段的末尾位置,这个末尾位置通常被称为program break,将program break向高地址移动可扩展进程的堆空间。对于较小的内存分配请求,malloc
的实现可能通过调用brk
来增加program break来获取一大块连续的虚拟内存,然后再内部管理这块内存,将其分割成小块以响应后续的malloc
请求,这样可以减少系统调用的次数。mmap
:用于在调用进程的虚拟地址空间中创建一个新的内存映射,对于较大的内存分配请求,通常是128k,malloc
一般会直接使用mmap
系统调用。使用mmap
分配的内存区域不一定与进程传统的堆区相邻,是文件映射区,在栈和堆中间。这块大内存是独立映射的,当它被free
时,可以通过munmap
系统调用直接将其归还给操作系统,避免了长期占用进程地址空间和可能产生的内存碎片问题。
free释放内存的细节?
- 对于
brk
分配的内存,free
通常只是将这块内存标记为“空闲”,并将其归还到malloc
内部的空闲列表中。它会尝试与相邻的空闲块进行合并,以形成更大的连续空闲块,减少内存碎片。这块内存通常不会立即归还给操作系统,而是留待后续的malloc
请求复用。 - 对于
mmap
分配的内存,free
通常会调用munmap
系统调用,将这块独立的内存区域直接、完整地归还给操作系统。
既然已经有了malloc和free,C++为什么还需要new和delete?
因为C++是一门面向对象的语言,new
和delete
的存在正是为了更好地支持面向对象特性中的对象生命周期的管理:
- 构造与析构:对象在创建时需要通过构造函数来初始化其成员变量、建立不变量并获取所需资源。在销毁时,需要通过析构函数来执行清理工作,如释放资源。
malloc
和free
对此一无所知,它们无法自动调用构造和析构函数。而new
和delete
则将内存分配、释放与对象的构造、析构紧密地集成在一起,确保了对象总能被正确地创建和销毁。 - 类型安全:
new
是类型安全的,它返回一个具有正确类型的指针,避免了malloc
所需的手动类型转换,从而减少了编程错误。 - 异常处理:
new
在失败时抛出异常,这与C++的错误处理机制(try-catch)无缝集成,使得代码更健壮、更清晰。 - 可扩展性:
new
和delete
可以被重载,为开发者提供了强大的定制内存管理策略的能力,这是malloc
和free
无法比拟的。
free是如何知道要释放多大内存的?delete[]又是如何知道要调用多少次析构函数的?
free
如何知道大小:当调用malloc
分配内存时,内存分配器实际上分配了一块比用户请求稍大的内存。在这块内存的头部,即返回给用户的指针地址之前的一个小区域,存储了一些元数据。这些元数据中就包含了该内存块的实际大小。当调用free
时,free
函数会根据传入的指针回退到头部位置,读取元数据,从而确切地知道需要释放的内存块的大小以及它在malloc
内部管理结构中的位置。delete[]
如何知道数组大小:当使用new T[N]
来分配一个对象数组时,除了可能像malloc
一样在头部存储整个内存块的大小信息外,通常还会额外存储数组的元素数量 N,这个数量通常被存储在返回给用户的指针所指向地址之前的一个固定偏移处(例如向前4个或8个字节)。当调用delete[]
时,delete[]
的实现首先会回退指针,找到并读取这个数量 N,然后会从数组的末尾开始,向前循环调用N次析构函数。这就是new[]
必须与delete[]
配对使用的原因,如果混用,只会调用第一个对象的析构函数,导致后面的N-1
个对象发生资源泄漏。
delete 一个空指针 (nullptr 或 NULL) 会怎么样?
C++标准明确规定,对空指针执行delete
或delete[]
操作是一个无操作 (no-op),它不会做任何事情,也不会导致程序崩溃。这个特性非常有用,因为它简化了代码,使得我们不必在每次调用delete
之前都检查指针是否为空:
// 冗余的检查
if (ptr != nullptr) {
delete ptr;
}
// 简洁且安全的写法
delete ptr; // 如果 ptr 是 nullptr,这里什么也不做
cpp对一个指针 delete 两次会发生什么?
对一个指针delete两次会导致未定义行为,程序可能会因为内存访问冲突(段错误)而终止,第二次delete可能会破坏内存管理器的内部数据结构,程序不会立即崩溃,后续再申请内存时,由于堆内存结构已经被破坏,会导致莫名的崩溃或者数据错乱。
双重删除避免的传统方法:
MyClass* p = new MyClass();
delete p;
p = nullptr;//把指针置为空,这是一个好习惯
// 后续如果不小心再次 delete
delete p; // 这里 p 是 nullptr,所以这行代码是安全的,什么也不会发生。
cpp使用指针指针,能够避免手动管理内存,避免发生这种问题:
#include <memory>
void myFunction() {
std::unique_ptr<MyClass> p = std::make_unique<MyClass>();
// ... 使用 p ...
} // p 离开作用域,MyClass 对象被自动删除,绝不会发生双重删除
cpp