今日学习:C++ 内存管理

从栈到堆,从 new/delete 到智能指针——理解内存布局与生命周期,是成为客户端架构师的必修课。

01

今日主题

你已经掌握了 RAII 和智能指针的"用法",今天补上"原理":对象到底存在哪里?new 背后发生了什么?为什么 C++ 程序的内存问题比 Web 应用多一个量级?内存管理是 C++ 与 JS 最大的认知鸿沟——JS 开发者从不需要思考栈和堆,而 C++ 开发者每写一行代码都在和内存布局打交道。

02

核心知识

进程的虚拟内存布局(从高地址到低地址):栈 → 堆 → BSS → 数据段 → 代码段。

栈(Stack):函数调用自动分配/释放,速度极快(一条指令移动栈指针),容量有限(默认 8MB,Linux)。局部变量、函数参数都在栈上。

堆(Heap):new/malloc 手动分配,生命周期由开发者控制,速度慢(需要系统调用或内存池管理),容量大(受虚拟内存限制)。

new Foo() 背后做了三件事:调用 operator new 分配原始内存 → 调用 Foo 构造函数 → 返回类型化指针。delete 则是:调用析构函数 → 释放内存。

关键认知:栈对象自动析构(RAII 的基础),堆对象必须手动释放。智能指针的本质就是"把堆对象包装成栈对象",利用栈的自动析构机制触发堆的释放。

03

与 Web 技术的联系

JS 引擎替你管理了堆。V8 的堆分配对象 + GC 自动回收,你从不关心内存布局。C++ 把这块遮羞布掀开了。

类比 JS 内存模型:

  • JS 原始类型(number/boolean)→ 栈上的值
  • JS 对象 → 堆上的引用,变量只是指针(和 C++ 指针本质相同)
  • JS GC 的"可达性分析" → C++ 的 shared_ptr 引用计数(都是"没人引用就回收")
  • JS 闭包捕获 → C++ lambda 按值/按引用捕获([=] vs [&],悬垂引用对应 JS 的闭包陷阱)

最大差异:JS 的 GC 是"不确定的、自动的",C++ 的 RAII 是"确定的、手动的"。这正是 Qt 代码里大量使用 QObject 父子关系管理生命周期的原因——Qt 用对象树模拟了"自动回收"。

04

实际案例

Qt 的对象树:在 Qt 中 new QPushButton(parent),子对象随父对象一起销毁。这就是把 C++ 的手动内存管理"半自动化"的经典设计——QObject 析构时自动 delete 所有 children。

Chrome 的 PartitionAlloc:Chrome 不用裸 malloc,而是自研分区分配器,把同类型对象放进独立分区,减少内存碎片并提升缓存命中率。大型客户端对内存的精细控制远超你的想象。

VSCode/Electron:Node.js 进程的堆由 V8 管理,但原生模块(如文件监听)依然要用 C++ 的内存管理——这就是为什么 Electron 崩溃往往和原生模块内存泄漏有关。

05

常见错误

1. 忘记 delete(泄漏):new 了不释放。JS 开发者最容易犯——因为 JS 不需要手动释放。✅ 用智能指针或 RAII 包裹。

2. 悬垂指针(use-after-free):delete 后还继续用。对应 JS 里几乎不会出现的错误。✅ 置空指针或用 weak_ptr。

3. 栈上返回局部对象引用:return &local; 函数结束栈就释放了。✅ 返回值或 std::move。

4. 深浅拷贝混淆:类含指针成员时默认拷贝是浅拷贝,析构两次导致 double-free。✅ 遵循"三五法则",或用 unique_ptr 成员让编译器禁止拷贝。

06

最佳实践

1. 优先栈对象:能用栈就不用堆。Foo f; 胜过 Foo* f = new Foo();。

2. 堆对象必须智能指针:C++17 起首选 std::unique_ptr(独占所有权);需要共享时用 shared_ptr,但注意循环引用要用 weak_ptr 打破。

3. Qt 场景:QWidget/QObject 用父子关系管理,不裸 delete;非 QObject 数据类用智能指针。

4. 不要用 new[]/delete[]:用 std::vector,它内部管理连续内存且自动释放。

07

今日思考题

为什么 JS 里对象赋值是"传引用",而 C++ 默认是"传值拷贝"?两种模型各自的代价是什么?

Qt 的父子对象树和 V8 的 GC 都解决"谁负责释放"的问题,它们的适用场景有什么本质区别?

如果让你设计一个内存池,你会怎么处理"释放后复用"和"碎片"问题?

08

今日实践任务(30分钟)

用智能指针重写一个泄漏代码

下面的代码有 3 处内存问题,请用 std::unique_ptr/std::shared_ptr 重写并解释每处修复原因:

struct Node {
  int value;
  Node* next = nullptr;
};

void build_list() {
  Node* head = new Node{1};
  Node* n2 = new Node{2};
  head->next = n2;
  // 函数结束:head 和 n2 泄漏!

  Node* tmp = head;   // 浅拷贝
  delete head;        // 释放 head
  std::cout << tmp->value; // use-after-free!
}

验证:用 g++ -fsanitize=address -g test.cpp 编译运行,观察 ASan 报告。修复后 ASan 应输出 ERROR: LeakSanitizer 为 0 泄漏。

一句话总结:C++ 的内存管理就是"明确每个对象的生命周期,并用 RAII 让它自动执行"。