动态内存

原理

进程内存布局

高地址  ┌─────────────┐
        │   栈 (Stack)  │ ← 向下增长,函数调用帧,自动管理
        ├─────────────┤
        │      ↓       │
        │   未使用区    │
        │      ↑       │
        ├─────────────┤
        │   堆 (Heap)   │ ← 向上增长,new/malloc 分配
        ├─────────────┤
        │   BSS 段     │ ← 未初始化全局变量(自动零初始化)
        ├─────────────┤
        │   数据段     │ ← 已初始化全局/静态变量
        ├─────────────┤
        │   代码段     │ ← 只读,机器指令
低地址  └─────────────┘

栈的分配只需移动栈指针(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):

; 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 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