动态内存

原理

进程内存布局

高地址 ┌─────────────┐
 │ 栈 (Stack) │ ← 向下增长,函数调用帧,自动管理
 ├─────────────┤
 │ ↓ │
 │ 未使用区 │
 │ ↑ │
 ├─────────────┤
 │ 堆 (Heap) │ ← 向上增长,new/malloc 分配
 ├─────────────┤
 │ BSS 段 │ ← 未初始化全局变量(自动零初始化)
 ├─────────────┤
 │ 数据段 │ ← 已初始化全局/静态变量
 ├─────────────┤
 │ 代码段 │ ← 只读,机器指令
低地址 └─────────────┘
```asm
 
栈的分配只需移动栈指针(rsp),极快(O(1))。堆的分配需要搜索空闲块并可能触发系统调用(brk/mmap),较慢且可能产生碎片。
 
### new 的底层调用链
 
`new T(args)` 在底层分为两步:
 
1. `operator new(sizeof(T))` → 内部调用 `malloc()` → 查找空闲链表 → 找不到则通过 `brk()` 或 `mmap()` 向 OS 申请
2. 在返回的原始内存上调用构造函数(placement new):`new (ptr) T(args)`
 
`delete` 则反过来:先调用析构函数,再 `operator delete(ptr)` → `free(ptr)`。
 
**汇编层验证**(x86-64):
```asm
; new Point(10, 20) 的汇编分解
mov edi, 8 ; sizeof(Point) = 8
call operator new ; → call malloc
mov rdi, rax ; rdi = this = 堆地址
mov esi, 10 ; 参数 x
mov edx, 20 ; 参数 y
call Point::Point(int,int) ; 在堆内存上构造
```asm
 
对于基础类型 `new int(42)`,由于 int 没有构造函数,编译器直接 `malloc(4)` + `mov [rax], 42`,不会生成函数调用。
 
### new[] 的 cookie 机制
 
`new T[n]` 分配对象数组时,编译器在返回给用户的指针**之前**存储一个 size_t(cookie),记录元素个数:
 
```cpp
[cookie: n][对象0][对象1]...[对象n-1]
 ↑ 返回给用户的指针
```cpp
 
`delete[]` 读取 cookie 确定析构函数调用次数(循环 n 次调用 `~T()`),然后将用户指针减去 cookie 大小后传给 `free()`。如果 T 没有析构函数(trivially destructible),编译器可能省略 cookie。
 
`new[]` 配 `delete`(漏 `[]`)的后果:
- 只析构第一个元素 → 其余元素资源泄漏
- free 的地址可能不对(未减去 cookie 偏移)→ 堆损坏或崩溃
 
### malloc/free 的内部实现
 
malloc 维护空闲链表(Free List),使用元数据结构跟踪每个块:
 
```cpp
struct malloc_chunk {
 size_t prev_size; // 前一个块的大小
 size_t size; // 当前块大小(最低位用作使用中标志)
 struct malloc_chunk* fd; // 前向指针(空闲链表中)
 struct malloc_chunk* bk; // 后向指针
 // ... 用户数据紧跟其后 ...
};
```cpp
 
分配策略:首次适配、最佳适配、下次适配。释放时检查相邻块是否为空闲以合并(coalescing),减少外部碎片。
 
### shared_ptr 控制块与 make_shared
 
`shared_ptr<T>` 内部维护一个控制块,包含引用计数(use_count)、弱引用计数(weak_count)和删除器。`make_shared<T>` 将对象和控制块合并为一次内存分配(sizeof(T) + 控制块大小),相比 `new T` + 构造 `shared_ptr`(两次分配)更高效。
 
`weak_ptr` 不增加 use_count,仅增加 weak_count。当 use_count 归零时对象被析构,但控制块保留直到 weak_count 也归零——因为 `weak_ptr::lock()` 需要检查对象是否仍然存活。
 
---
 
## 语法
 
### new / delete 基本用法
 
```cpp
int* p = new int(42); // 分配单个对象并初始化
delete p;
 
int* arr = new int[10]{}; // 分配数组,值初始化(全 0)
delete[] arr; // 数组必须用 delete[]
 
// nothrow new:分配失败返回 nullptr 而非抛异常
int* p2 = new (std::nothrow) int[1000000000];
```cpp
 
### placement new
 
在已分配的原始内存上构造对象,不分配新内存:
 
```cpp
char buffer[sizeof(Object) * 3];
Object* obj = new (buffer) Object(1, 2);
obj->~Object(); // 必须手动调用析构
// operator delete(buffer); // 或其他释放方式
```cpp
 
### unique_ptr
 
```cpp
auto up = std::make_unique<Resource>(args); // C++14
up->use();
auto up2 = std::move(up); // 转移所有权
// up.release() — 放弃所有权,返回裸指针(不调用 delete)
// up.reset(ptr) — 释放旧对象,接管新对象
 
// 自定义删除器
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("f.txt", "r"), fclose);
```cpp
 
### shared_ptr / weak_ptr
 
```cpp
auto sp = std::make_shared<Resource>(args);
auto sp2 = sp; // use_count = 2
sp.use_count(); // 查询引用计数
 
std::weak_ptr<Resource> wp = sp;
if (auto locked = wp.lock()) { // 提升为 shared_ptr
 locked->use(); // 安全使用
}
wp.expired(); // 检查是否失效
```cpp
 
### allocator
 
```cpp
std::allocator<std::string> alloc;
auto p = alloc.allocate(3); // 分配原始内存
alloc.construct(p, "Hello"); // 构造对象
alloc.destroy(p); // 析构对象
alloc.deallocate(p, 3); // 释放内存
```cpp
 
### Rule of Five
 
如果类自定义了析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值中的任意一项,通常需要定义全部五项:
 
```cpp
class Buffer {
 char* data; size_t size;
public:
 ~Buffer() { delete[] data; }
 Buffer(const Buffer& other); // 拷贝构造:深拷贝
 Buffer& operator=(const Buffer& other); // 拷贝赋值
 Buffer(Buffer&& other) noexcept; // 移动构造:窃取指针
 Buffer& operator=(Buffer&& other) noexcept; // 移动赋值
};
```cpp
 
> 不管理资源的类使用 Rule of Zero:完全依赖编译器默认生成。
 
---
 
## 实践
 
**练习**:链表题(链表与指针操作),约瑟夫环题(动态链表),二维数组题(动态二维数组)。