动态内存
原理
进程内存布局
高地址 ┌─────────────┐
│ 栈 (Stack) │ ← 向下增长,函数调用帧,自动管理
├─────────────┤
│ ↓ │
│ 未使用区 │
│ ↑ │
├─────────────┤
│ 堆 (Heap) │ ← 向上增长,new/malloc 分配
├─────────────┤
│ BSS 段 │ ← 未初始化全局变量(自动零初始化)
├─────────────┤
│ 数据段 │ ← 已初始化全局/静态变量
├─────────────┤
│ 代码段 │ ← 只读,机器指令
低地址 └─────────────┘
栈的分配只需移动栈指针(rsp),极快(O(1))。堆的分配需要搜索空闲块并可能触发系统调用(brk/mmap),较慢且可能产生碎片。
new 的底层调用链
new T(args) 在底层分为两步:
operator new(sizeof(T))→ 内部调用malloc()→ 查找空闲链表 → 找不到则通过brk()或mmap()向 OS 申请- 在返回的原始内存上调用构造函数(placement new):
new (ptr) T(args)
delete 则反过来:先调用析构函数,再 operator delete(ptr) → free(ptr)。
汇编层验证(x86-64):
; 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) ; 在堆内存上构造对于基础类型 new int(42),由于 int 没有构造函数,编译器直接 malloc(4) + mov [rax], 42,不会生成函数调用。
new[] 的 cookie 机制
new T[n] 分配对象数组时,编译器在返回给用户的指针之前存储一个 size_t(cookie),记录元素个数:
[cookie: n][对象0][对象1]...[对象n-1]
↑ 返回给用户的指针
delete[] 读取 cookie 确定析构函数调用次数(循环 n 次调用 ~T()),然后将用户指针减去 cookie 大小后传给 free()。如果 T 没有析构函数(trivially destructible),编译器可能省略 cookie。
new[] 配 delete(漏 [])的后果:
- 只析构第一个元素 → 其余元素资源泄漏
- free 的地址可能不对(未减去 cookie 偏移)→ 堆损坏或崩溃
malloc/free 的内部实现
malloc 维护空闲链表(Free List),使用元数据结构跟踪每个块:
struct malloc_chunk {
size_t prev_size; // 前一个块的大小
size_t size; // 当前块大小(最低位用作使用中标志)
struct malloc_chunk* fd; // 前向指针(空闲链表中)
struct malloc_chunk* bk; // 后向指针
// ... 用户数据紧跟其后 ...
};分配策略:首次适配、最佳适配、下次适配。释放时检查相邻块是否为空闲以合并(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 基本用法
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];placement new
在已分配的原始内存上构造对象,不分配新内存:
char buffer[sizeof(Object) * 3];
Object* obj = new (buffer) Object(1, 2);
obj->~Object(); // 必须手动调用析构
// operator delete(buffer); // 或其他释放方式unique_ptr
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);shared_ptr / weak_ptr
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(); // 检查是否失效allocator
std::allocator<std::string> alloc;
auto p = alloc.allocate(3); // 分配原始内存
alloc.construct(p, "Hello"); // 构造对象
alloc.destroy(p); // 析构对象
alloc.deallocate(p, 3); // 释放内存Rule of Five
如果类自定义了析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值中的任意一项,通常需要定义全部五项:
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; // 移动赋值
};不管理资源的类使用 Rule of Zero:完全依赖编译器默认生成。
实践
力扣题目:力扣链表题(链表与指针操作),力扣约瑟夫环(动态链表),力扣二维数组题(动态二维数组)。
AI 自检:1) 画出 new Point[3] 的堆内存布局(含 cookie);2) 解释为什么 new[] 和 delete 混用会导致堆损坏;3) shared_ptr 的引用计数存放在哪里?make_shared 相比 new shared_ptr 的优势体现在哪些汇编指令上?
建议先阅读:03_指针与引用 — 指针是动态内存操作的基础;05_面向对象(一)类与对象基础 — 构造/析构函数在动态内存中的角色。
内存泄漏检测:
g++ -fsanitize=address -g program.cpp -o program
valgrind --leak-check=full ./program