Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

内存管理 / Memory Management

1. C++ 内存管理总览:先看完整地图 / C++ Memory Management Map

2. 程序内存布局:内存不是只有堆和栈 / Program Memory Layout

3. 栈与函数调用:局部变量为什么会消失 / Stack and Function Calls

4. 堆与动态分配:new 到底做了什么 / Heap and Dynamic Allocation

5. 对象生命周期:地址还在不代表对象还活着 / Object Lifetime

6. RAII 与所有权:让对象负责清理 / RAII and Ownership Design

7. 智能指针与所有权关系:谁拥有,谁只是看 / Smart Pointers and Ownership Graphs

8. 容器、迭代器与内存失效 / Containers, Iterators, and Invalidation

9. allocator 与 PMR:把分配策略抽出来 / Allocators and PMR

10. 内存错误与 Sanitizer:把崩溃变成证据 / Memory Errors and Sanitizers

本页目录

堆与动态分配:new 到底做了什么 / Heap and Dynamic Allocation ​

堆不是“高级一点的栈”。

堆解决的是:对象大小或存在时间在编译时、在当前作用域里不好固定。代价是:你必须明确谁负责清理。

1. 为什么需要动态分配 ​

栈上的局部对象适合这种场景:

cpp
void f() {
    int value = 42;
}
1
2
3

对象随函数进入而创建,随函数离开而销毁。

这很清楚,也很高效。

但有些问题不适合只靠栈:

text
运行时才知道要多少元素
对象需要跨越当前函数继续存在
容器需要不断增长
对象太大,不适合放在栈帧中
需要通过多态接口创建派生类对象
1
2
3
4
5

例如:

cpp
#include <vector>

std::vector<int> read_values(int n) {
    std::vector<int> values;
    for (int i = 0; i < n; ++i) {
        values.push_back(i);
    }
    return values;
}
1
2
3
4
5
6
7
8
9

values 这个 vector 对象本体是局部对象。

但它内部元素缓冲区会根据需要动态申请。

text
vector object
  |
  v
dynamic buffer: [0][1][2][3]...
1
2
3
4

这就是堆常见出现位置:你未必手写 new,但标准库容器可能在内部使用动态存储。

2. new 至少做两件事 ​

看代码:

cpp
int* p = new int(42);
1

不要只把它理解成“在堆上弄了个 int”。

更准确的步骤是:

text
1. 调用分配函数,申请足够放 int 的原始存储
2. 在那块存储上构造 int 对象,初值为 42
3. 返回这个对象的地址
1
2
3

图:

text
new int(42)
  |
  +-- allocate storage
  |
  +-- construct int object
  |
  v
return pointer
1
2
3
4
5
6
7
8

对应的:

cpp
delete p;
1

也至少做两件事:

text
1. 调用析构逻辑,结束对象生命周期
2. 释放那块动态存储
1
2

对 int 来说析构几乎没有存在感。

换成 std::string 就更明显:

cpp
#include <string>

std::string* p = new std::string("hello");
delete p;
1
2
3
4

delete p 必须让 std::string 的析构函数执行,否则它内部拥有的资源可能无法正确释放。

3. 分配和构造不是一回事 ​

C++ 初学阶段很容易把这两个动作合在一起。

但后面学习 allocator、placement new、容器实现时,必须拆开。

text
allocation
  只关心字节:给我一块足够大、对齐合适的存储

construction
  关心类型:在这块存储上真正创建一个 T 对象
1
2
3
4
5

可以把它想成盖房子:

text
土地
  像原始存储

房子
  像对象

有土地不等于已经有房子。
房子拆了,土地也不一定立刻消失。
1
2
3
4
5
6
7
8

这不是比喻游戏,它对应真实 C++ 机制。

std::vector 扩容时,往往会:

text
申请更大的原始存储
  |
  v
把旧元素移动或复制构造到新存储
  |
  v
析构旧元素
  |
  v
释放旧存储
1
2
3
4
5
6
7
8
9
10

4. malloc/free 和 new/delete 的区别 ​

C 语言常用:

cpp
#include <cstdlib>

int* p = static_cast<int*>(std::malloc(sizeof(int)));
std::free(p);
1
2
3
4

malloc 只申请原始字节。

它不会调用 C++ 构造函数。

free 只释放原始字节。

它不会调用 C++ 析构函数。

对平凡类型也许暂时看不出差别。

对 std::string 这种类型就危险:

cpp
#include <cstdlib>
#include <string>

void bad() {
    void* raw = std::malloc(sizeof(std::string));
    std::string* p = static_cast<std::string*>(raw);

    // 这里还没有真正构造 std::string 对象
    // 直接使用 *p 是错误的

    std::free(raw);
}
1
2
3
4
5
6
7
8
9
10
11
12

这就是为什么 C++ 普通对象不要随便用 malloc/free 管。

5. new[] 和 delete[] 必须配对 ​

数组动态分配:

cpp
int* values = new int[10]{};
delete[] values;
1
2

这里必须用 delete[]。

不能写:

cpp
delete values; // 错
1

原因是数组里有多个元素。

对非平凡类型,每个元素都需要析构。

delete[] 才表达“这是数组形式分配的对象序列”。

不过现代 C++ 里,普通代码更推荐:

cpp
#include <vector>

std::vector<int> values(10);
1
2
3

容器会负责处理大小、析构和释放。

6. 裸 new 的问题不是它邪恶,而是责任不清 ​

看代码:

cpp
Widget* create_widget() {
    return new Widget{};
}
1
2
3

调用者必须知道:

text
这个指针需要 delete 吗?
什么时候 delete?
异常发生时谁 delete?
能不能传给别人?
能不能保存起来?
1
2
3
4
5

裸指针没有把答案写在类型里。

更清楚的接口是:

cpp
#include <memory>

std::unique_ptr<Widget> create_widget() {
    return std::make_unique<Widget>();
}
1
2
3
4
5

这个返回类型直接表达:

text
调用者得到一个独占拥有者。
它离开作用域时会自动释放对象。
1
2

7. 动态分配的常见错误 ​

内存泄漏:

cpp
void leak() {
    int* p = new int(42);
    // 忘记 delete
}
1
2
3
4

重复释放:

cpp
int* p = new int(42);
delete p;
delete p;
1
2
3

释放后使用:

cpp
int* p = new int(42);
delete p;
int x = *p;
1
2
3

配对错误:

cpp
int* values = new int[10];
delete values; // 应该 delete[]
1
2

这些错误的共同点是:

text
动态存储的清理责任没有被一个明确对象稳定持有。
1

8. 本篇总结 ​

堆解决灵活性问题:

text
大小运行时决定
对象跨作用域存在
容器扩容
多态对象创建
大块数据存储
1
2
3
4
5

但堆带来清理责任:

text
new
  分配存储 + 构造对象

delete
  析构对象 + 释放存储
1
2
3
4
5

现代 C++ 的基本方向是:

text
少在业务代码里裸写 new/delete。
用容器、string、unique_ptr、shared_ptr、RAII 类型表达所有权。
1
2

下一篇讲对象生命周期。因为很多内存错误不是“不知道 delete”,而是“不知道对象什么时候已经不活了”。

最后更新于:

Pager
上一篇3. 栈与函数调用:局部变量为什么会消失 / Stack and Function Calls
下一篇5. 对象生命周期:地址还在不代表对象还活着 / Object Lifetime

持续记录,持续成长

Copyright © Tidenflow