【C++面试题】malloc和new delete和delete[]
文章目录
- 一、malloc和new
- 1. `malloc` 的底层实现
- 1.1 内存管理
- 1.2 系统调用
- 1.3 内存对齐
- 2. `new` 的底层实现
- 2.1 内存分配
- 2.2 构造函数调用
- 2.3 异常处理
- 3. `malloc` 和 `new` 的区别
- 4. 总结
- 二、delete和delete[]
- 1. `delete` 和 `delete[]` 的区别
- 1.1 `delete` 的行为
- 1.2 `delete[]` 的行为
- 2. 错误使用 `delete` 和 `delete[]` 的后果
- 2.1 使用 `delete` 释放 `new[]` 分配的数组
- 2.2 使用 `delete[]` 释放 `new` 分配的单个对象
- 3. 底层机制
- 3.1 `new` 和 `delete` 的底层
- 3.2 `new[]` 和 `delete[]` 的底层
- 3.3 内存布局示例
- 4. 如何避免错误
- 4.1 匹配使用
- 4.2 使用智能指针
- 5. 总结
一、malloc和new
1. malloc
的底层实现
malloc
是 C 标准库中的函数,用于在堆上分配指定大小的内存块。其底层实现通常依赖于操作系统的内存管理机制。
1.1 内存管理
- 堆内存管理:
malloc
从堆(heap)中分配内存。堆是一个由操作系统管理的内存区域,程序可以通过malloc
和free
来动态分配和释放内存。 - 内存块管理:
malloc
通常会维护一个空闲内存块链表(free list),用于跟踪哪些内存块是空闲的。当调用malloc
时,它会遍历这个链表,找到一个足够大的空闲块来满足请求。
1.2 系统调用
brk
和sbrk
:在某些系统中,malloc
使用brk
或sbrk
系统调用来调整堆的大小。这些系统调用会改变程序的“break”指针,从而扩展或收缩堆的大小。mmap
:对于较大的内存分配,malloc
可能会使用mmap
系统调用,直接将内存映射到进程的地址空间。这种方式可以避免频繁调整堆的大小。
1.3 内存对齐
malloc
通常会返回对齐的内存块,以满足硬件或操作系统的对齐要求。例如,在 64 位系统上,malloc
可能会返回 8 字节对齐的内存。
2. new
的底层实现
new
是 C++ 中的操作符,用于动态分配内存并调用对象的构造函数。它的底层实现通常依赖于 malloc
或类似的内存分配机制。
2.1 内存分配
- 调用
malloc
:在大多数实现中,new
操作符会调用malloc
来分配内存。new
不仅分配内存,还会调用对象的构造函数来初始化对象。 - 内存大小:
new
会根据对象的大小来分配内存,通常还会额外分配一些内存用于存储元数据(如对象的大小或类型信息)。
2.2 构造函数调用
- 对象初始化:
new
操作符在分配内存后,会自动调用对象的构造函数来初始化对象。这是new
与malloc
的主要区别之一。
2.3 异常处理
- 异常抛出:如果内存分配失败,
new
会抛出std::bad_alloc
异常(除非使用了nothrow
版本的new
)。这与malloc
不同,malloc
在分配失败时返回NULL
。
3. malloc
和 new
的区别
特性 | malloc | new |
---|---|---|
语言 | C | C++ |
内存分配 | 仅分配内存 | 分配内存并调用构造函数 |
内存释放 | free | delete |
失败处理 | 返回 NULL | 抛出 std::bad_alloc 异常 |
内存对齐 | 通常对齐到最大基本类型大小 | 通常对齐到对象的最大对齐要求 |
内存大小 | 需要手动计算 | 自动计算对象大小 |
4. 总结
malloc
是 C 标准库中的函数,底层依赖于操作系统的内存管理机制(如brk
、sbrk
或mmap
),主要用于分配原始内存块。new
是 C++ 中的操作符,底层通常调用malloc
来分配内存,并自动调用对象的构造函数来初始化对象。
两者在内存分配和管理上有相似之处,但 new
提供了更高层次的功能,特别是在对象构造和异常处理方面。
在 C++ 中,delete
和 delete[]
是用于释放动态分配内存的操作符,但它们的行为和用途有所不同。正确使用它们非常重要,否则可能导致未定义行为(undefined behavior)。以下是对 delete
和 delete[]
的详细说明,以及错误使用它们(如 new
和 delete[]
混用)的后果。
二、delete和delete[]
1. delete
和 delete[]
的区别
操作符 | 用途 | 适用场景 |
---|---|---|
delete | 释放单个对象的内存,并调用单个对象的析构函数。 | 用于释放 new 分配的对象。 |
delete[] | 释放对象数组的内存,并依次调用数组中每个对象的析构函数。 | 用于释放 new[] 分配的数组。 |
1.1 delete
的行为
- 用于释放通过
new
分配的单个对象。 - 调用对象的析构函数。
- 释放对象占用的内存。
1.2 delete[]
的行为
- 用于释放通过
new[]
分配的对象数组。 - 依次调用数组中每个对象的析构函数。
- 释放整个数组占用的内存。
2. 错误使用 delete
和 delete[]
的后果
2.1 使用 delete
释放 new[]
分配的数组
int* arr = new int[10];
delete arr; // 错误:应该使用 delete[]
- 后果:
- 只会调用第一个元素的析构函数(如果元素是类对象)。
- 内存释放不完整,可能导致内存泄漏。
- 未定义行为(undefined behavior),程序可能崩溃或产生不可预测的结果。
2.2 使用 delete[]
释放 new
分配的单个对象
int* p = new int;
delete[] p; // 错误:应该使用 delete
- 后果:
- 程序会尝试释放一个不存在的数组,可能导致堆损坏。
- 未定义行为(undefined behavior),程序可能崩溃或产生不可预测的结果。
3. 底层机制
3.1 new
和 delete
的底层
new
和delete
的实现通常依赖于malloc
和free
。new
不仅分配内存,还会调用构造函数。delete
不仅释放内存,还会调用析构函数。
3.2 new[]
和 delete[]
的底层
new[]
会分配额外的内存来存储数组的大小(通常是在数组开头),以便delete[]
知道需要调用多少次析构函数。delete[]
会根据数组大小依次调用每个元素的析构函数,然后释放整个数组的内存。
3.3 内存布局示例
假设有一个类 MyClass
,使用 new[]
分配数组:
MyClass* arr = new MyClass[10];
内存布局可能如下:
[数组大小(10)][MyClass 对象 1][MyClass 对象 2]...[MyClass 对象 10]
delete[]
会从数组大小中读取值(10),然后依次调用 10 个对象的析构函数,最后释放整个内存块。
如果错误地使用 delete
:
- 程序只会尝试释放第一个对象的内存,而不会释放整个数组,导致内存泄漏和未定义行为。
4. 如何避免错误
4.1 匹配使用
- 使用
new
分配的内存,必须用delete
释放。 - 使用
new[]
分配的内存,必须用delete[]
释放。
4.2 使用智能指针
- 使用
std::unique_ptr
或std::shared_ptr
可以避免手动管理内存。 - 对于数组,可以使用
std::unique_ptr<T[]>
或std::vector
。
示例:
// 使用 std::unique_ptr 管理单个对象
std::unique_ptr<MyClass> p(new MyClass);
// 使用 std::unique_ptr 管理数组
std::unique_ptr<MyClass[]> arr(new MyClass[10]);
// 使用 std::vector 管理数组
std::vector<MyClass> vec(10);
5. 总结
操作符 | 正确使用场景 | 错误使用场景 | 后果 |
---|---|---|---|
delete | 释放 new 分配的单个对象 | 释放 new[] 分配的数组 | 内存泄漏、未定义行为 |
delete[] | 释放 new[] 分配的数组 | 释放 new 分配的单个对象 | 堆损坏、未定义行为 |
- 始终匹配使用:
new
配delete
,new[]
配delete[]
。 - 优先使用智能指针:避免手动管理内存,减少错误发生的可能性。