核心答案:
delete
和 free()
不需要我们指定大小,是因为内存分配器在分配内存时,已经暗中记录下了这块内存的大小等元信息(通常记录在分配的内存块之前或之后)。释放时,内存分配器通过我们提供的指针,反向查找这些元信息,从而知道需要释放多少内存。
详细原理与底层实现
这个过程可以分解为以下几个关键点:
1. 内存分配器的幕后工作
当你调用 new
或 malloc()
时,内存分配器所做的远不止是找到一块空闲内存给你。
- 请求与分配:你请求
N
字节的内存。 - 记录元数据:分配器会分配一块大于
N
字节的内存块。这多出来的部分用于存储元数据。元数据通常包括:- 分配的内存块大小
- 用于调试的信息(如分配所在的文件、行号)
- 用于维护内存块链表结构的指针(如前一个块、后一个块)
- 校验和或魔术数字(用于检测内存损坏)
- 返回指针:分配器将指向用户数据区起始地址的指针返回给你。这个地址通常是“元数据起始地址 + 元数据大小”。
假设元数据需要 M
字节,分配器实际分配的内存结构在底层大致如下:
低地址方向
...
——————————————————————————————————————————————
| 元数据 (M字节) | 用户数据区 (N字节) | 填充/对齐 |
——————————————————————————————————————————————
^ ^
| |
元数据起始地址 返回给用户的指针 (ptr)
(alloc_ptr)
图:内存分配器返回的指针 (ptr
) 指向用户数据区,分配器通过 ptr - M
来定位元数据。
2. 释放时的逆向操作
当你调用 delete ptr
或 free(ptr)
时:
- 查找元数据:内存分配器接收到的
ptr
是你提供的、指向用户数据区的指针。分配器根据预定义的规则(例如,ptr - M
),找到这块内存对应的元数据起始地址。 - 读取信息:从元数据中读取当初分配的大小
N
以及其他信息(用于完整性检查)。 - 执行释放:分配器现在知道了要释放的内存块的确切起始地址和大小,于是可以将这块内存标记为空闲,并回归到空闲内存池中,供后续分配使用。同时,它可能会检查校验和以确保内存没有被意外破坏(在调试模式下)。
3. 一个简单的类比
这就像你去停车场停车:
- 分配 (
malloc
/new
):管理员给你一张票(指针),并在他自己的本子(元数据)上记录下:“票号A123,对应车位B区05号(大小)”。 - 释放 (
free
/delete
):你归还票A123。管理员不需要你告诉他你的车有多大,他根据票号A123去查他的本子,就知道要去B区05号车位把车移走(释放),并且那个车位能停多大的车(大小)他也一清二楚。
深入探讨与重要细节
1. C++ 的 delete
与 delete[]
对于简单类型(如 int
),delete
和 delete[]
通常可以混用,因为释放内存只需要知道起始地址和大小。
但对于类对象数组,它们绝不能混用。原因如下:
- 当你使用
new MyClass[10]
时,分配器除了分配10个对象的内存,还可能分配额外的空间来存储数组的元素个数(这是另一种元数据),供delete[]
使用。 delete[] ptr
:它会根据存储的数组元素个数n
,对每个对象(ptr[0]
到ptr[n-1]
)调用析构函数,然后再释放整个内存块。delete ptr
:它只会对ptr[0]
调用析构函数,然后释放内存。这将导致后面9个对象的析构函数未被调用(资源泄漏),并且释放的地址可能也不对(因为new[]
返回的地址可能不是分配的真实起始地址)。
这就是为什么必须配对使用 new/delete
和 new[]/delete[]
。
2. 为什么不能 free()
一个 new
来的指针,反之亦然?
new
/delete
和 malloc()
/free()
可能使用不同的内存分配器。
- C++ 的
new
/delete
通常会使用它自己的运行时库来进行分配和释放。 - C 的
malloc()
/free()
使用 C 运行时库的分配器。
这两个分配器记录元数据的方式、位置和格式很可能完全不同。用 free()
去释放一个由 new
分配的对象,free()
会试图用C分配器的规则去解析它认为的“元数据”,结果必然是解析出一堆垃圾信息,导致未定义行为(通常是程序崩溃)。
3. 内存分配器的实现策略
不同的分配器记录元数据的方式不同,常见策略有:
- 显式长度存储:在最常见的实现中,元数据(包括大小)就存储在返回给用户的指针之前的几个字节。
- segregated free lists:分配器维护多个不同大小的内存块链表。释放时,分配器可以根据指针所在的内存区域推断出它所属的块大小类别。
总结
操作 | 用户视角 | 内存分配器视角 |
---|---|---|
分配 (new /malloc ) | 请求大小 N ,得到一个指针 ptr 。 | 分配 N + M 字节。记录大小等元数据。返回 ptr 。 |
释放 (delete /free ) | 提供指针 ptr 。 | 通过 ptr 找到元数据,读出大小 N 。释放 N + M 字节。 |
所以,内存分配器通过“暗中记录”的方式,让用户从手动记录大小的繁琐和易错中解放出来。这是一种经典的“以空间换时间”和“抽象化”的设计,极大地简化了程序员的工作。但同时,它也要求我们必须遵守规则:分配和释放的方式必须匹配,否则分配器精心维护的元数据就会被破坏,导致灾难性后果。