ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小学生学C++编程语法知识(C++中new和delete的实现原理)

小学生学C++编程语法知识(C++中new和delete的实现原理)


要理解C++ 动态内存、new[]delete[]、内存分配器,甚至 STL 容器底层原理

先告诉同学们,一个非常有趣的事实:

delete p表面上只拿到了一个地址,为什么它居然知道要释放多大的内存?

因为真正负责管理内存的“内存管理员”,通常会在这块内存附近偷偷保存一些管理信息。

不过这里有一个非常重要的前提:

C++ 标准并没有规定new/delete的底层内存布局必须是什么样。

所以我们下面讲的是典型实现的原理,不是说所有编译器都一定这样实现。


一、先从一个最简单的问题开始

看:

int *p = new int;

同学们可能会问:

new int到底发生了什么?

可以把它想成去仓库找房子。

程序 │ │ new int ↓ 内存管理员 │ ↓ 找一块能够放 int 的内存 │ ↓ 创建 int 对象 │ ↓ 返回地址

最后:

int *p = new int;

得到:

p ↓ ┌──────────┐ │ int 对象 │ └──────────┘

二、new并不是一个普通函数

这是第一个非常重要的知识点。

很多初学者会认为:

new int

就是调用某个叫new的函数。

实际上不是。

new是 C++ 的运算符/语言机制

它背后通常会涉及:

operator new()

例如可以粗略理解成:

int *p = new int;

背后大概经历:

① operator new(sizeof(int)) ↓ ② 得到一块原始内存 ↓ ③ 在这块内存上构造 int 对象 ↓ ④ 返回对象地址

所以:

new≈ “申请内存 + 创建对象”


三、那么operator new()是干什么的?

例如:

operator new(sizeof(int));

可以理解为:

“内存管理员,请给我一块足够大的原始内存。”

它最终通常会调用更底层的内存管理机制,例如:

C++ new ↓ operator new ↓ 运行库的内存分配器 ↓ 操作系统/堆管理器 ↓ 得到内存

注意:

不是每次new都直接向操作系统申请一块内存。

如果每个:

new int

都跑到操作系统去申请,速度会非常慢。

实际上通常有更复杂的堆管理器、内存池、缓存机制


四、那delete又做了什么?

假设:

int *p = new int; delete p;

可以粗略理解成:

delete p ↓ 找到 p 指向的对象 ↓ 销毁对象 ↓ 释放这块内存

对于一个简单的:

int

对象的销毁非常简单。

但如果是:

class Student { public: ~Student() { cout << "Bye!"; } };

那么:

Student *p = new Student; delete p;

还需要调用:

Student::~Student()

所以:

delete不只是“把内存扔掉”,它还负责对象生命周期的结束。


五、最神奇的问题来了

现在:

int *p = new int;

然后:

delete p;

同学们就会问:

🤔delete只有一个p

p里面只有一个地址!

那它怎么知道这块内存到底有多大?

比如:

p = 0x1000

它只知道:

地址 = 0x1000

但是:

到底占4字节? 到底占40字节? 还是400字节?

怎么办?


六、答案:真正的“内存管理员”知道

这里要理解一个重要区别:

p

只是一个指针

它本身通常只保存:

地址

但是:

内存管理器

手里有自己的账本。

可以想象成:

内存管理员的账本 地址 大小 ──────────────────────── 0x1000 32 字节 0x1050 128 字节 0x1200 64 字节

当你:

delete p;

实际上会把:

0x1000

交给内存管理器。

内存管理器:

“哦,0x1000 是我之前分出去的一块32字节内存,我知道怎么处理。”

于是:

p ↓ 0x1000 ↓ 内存管理器 ↓ 找到对应的内存块 ↓ 释放

七、但是“账本”放在哪里?

这里就非常有意思了。

典型的堆管理器可能会在内存块附近保存一些管理信息

例如为了方便理解,可以想象:

内存块 管理信息 ↓ ┌──────────────┬───────────────────┐ │ size = 32 │ 用户数据 │ └──────────────┴───────────────────┘ ↑ p

也就是说:

p ↓ 用户真正使用的区域

而前面可能还有:

size 状态 链表指针 对齐信息 ……

这些东西属于内存分配器的管理信息


八、注意:这不意味着p[-1]就一定是大小

千万不要让小朋友产生这个误解:

int *p = new int; cout << p[-1]; // ❌不要这样猜

C++ 标准并没有规定:

“new 出来的内存前面一定存着 size。”

具体怎么保存,是:

  • 编译器

  • C++ 运行库

  • 堆分配器

  • 操作系统

等共同决定的实现细节。

我们为了理解原理,才画成:

┌─────────────┬──────────────┐ │ 管理信息 │ 用户数据 │ └─────────────┴──────────────┘ ↑ p

九、那new[]更神奇

比如:

int *p = new int[10];

这里发生了:

申请一大片空间 + 创建10个 int 对象

内存可以想象成:

┌─────────────┬────────────────────────────┐ │ 管理信息 │ 10个 int 对象 │ │ │ │ │ count = 10 │ [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]│ └─────────────┴────────────────────────────┘ ↑ p

这里的:

count = 10

只是典型实现中可能存在的一种做法

它让运行时知道:

“这里原来有10个对象。”


十、为什么delete[] p能知道是10个?

因为:

int *p = new int[10];

典型实现可能在返回给你的指针附近记录:

元素数量 = 10

所以:

delete[] p;

大致可以理解为:

delete[] p ↓ 找到这块分配 ↓ 知道原来有10个对象 ↓ 依次销毁对象 ↓ 释放整块内存

如果对象是:

Student

那么尤其重要。

比如:

Student *p = new Student[10]; delete[] p;

程序需要知道:

Student 1 → 析构 Student 2 → 析构 …… Student 10 → 析构

所以new[]/delete[]的实现通常需要某种方式记录数组的信息。


十一、这就是为什么deletedelete[]不能混用

例如:

Student *p = new Student[10]; delete p; // ❌

这相当于:

“我申请了10个人,但是我告诉系统只处理一个对象。”

这可能导致:

  • 没有正确调用所有析构函数

  • 内存释放方式不匹配

  • 未定义行为

所以一定要:

Student *p = new Student[10]; delete[] p;

十二、但是有一个特别重要的细节

很多教程会告诉同学们:

new[]会在数组前面保存数组长度,delete[]就从那里读取长度。”

这个说法:

用来理解非常好,但不能当成 C++ 标准规定。

实际上:

C++ 标准只规定行为,不规定编译器必须怎样实现。

有的实现可能:

size count ……

存在某个隐藏区域。

有的实现可能采用其他方式。

有的类型还可能有更特殊的优化。

所以我们应该记住:

“数组长度需要被实现机制记住”是重要的原理;“一定存在某个固定位置保存 count”不是标准保证。


十三、那么delete到底“知道多少字节”?

这里还要纠正一个非常容易产生的误区。

我们说:

int *p = new int; delete p;

并不是说:

delete自己通过sizeof(*p)算出内存块到底申请了多少字节。

实际上:

sizeof(*p)

只能告诉编译器:

一个 int 对象有多大

例如:

sizeof(int)

可能是:

4

但这和:

这次内存分配到底从堆管理器拿到了多大的块

不是完全一回事。

真正的内存块管理通常由:

operator delete ↓ 运行库/堆分配器

负责。


十四、可以把整个过程画成一条流水线

这是理解new/delete最重要的一张图:

new Student │ ↓ ┌──────────────────┐ │ operator new │ └──────────────────┘ │ ↓ 内存分配器/堆 │ ↓ ┌───────────────────┐ │ 管理信息 + 内存块 │ └───────────────────┘ │ ↓ 构造 Student │ ↓ 返回 p

然后:

delete p │ ↓ 析构 Student │ ↓ operator delete │ ↓ 堆内存管理器 │ ↓ 回收这块内存

所以:

new是“申请 + 构造”,delete是“析构 + 释放”。


十五、一个非常重要的思想:对象 ≠ 内存

这是小朋友学习 C++ 时非常重要的一关。

例如:

Student *p = new Student;

实际上可以分成:

第一步: 申请一块原始内存 第二步: 在这块内存上创建 Student 对象

也就是:

内存 ↓ ┌────────────────┐ │ 一块原始空间 │ └────────────────┘ ↓ 构造 ┌────────────────┐ │ Student 对象 │ └────────────────┘

而:

delete p;

则是反过来:

Student 对象 ↓ 调用析构函数 ↓ 对象结束 ↓ 释放底层内存

这也是为什么上一节我们讲的allocator很重要。


十六、allocator就把这两个动作拆开了

例如:

allocator<Student> a;

先:

Student *p = a.allocate(1);

此时:

有内存 但还没有 Student 对象

然后:

a.construct(p);

才创建对象。

最后:

a.destroy(p);

销毁对象。

再:

a.deallocate(p, 1);

释放内存。

于是整个生命周期变成:

allocate ↓ 原始内存 ↓ construct ↓ Student对象 ↓ destroy ↓ 对象消失 ↓ deallocate ↓ 内存归还

这就把new/delete背后的秘密彻底拆开了。


十七、还有一个很容易被忽略的问题

同学们可能会问:

“如果delete需要知道内存大小,那为什么我们写delete p的时候不用写大小?”

例如:

int *p = new int; delete p;

而不是:

delete p, sizeof(int);

原因就是:

内存管理器自己负责管理分配信息。

这也是内存分配器存在的重要意义。

你只需要告诉它:

“我要释放这个地址。”

剩下的:

“这块内存属于谁?” “多大?” “怎么放回空闲区?”

由内存管理器处理。

就像去图书馆还书:

你:我要还这本书。 管理员:好的,我知道这本书属于哪个书架。

你不用告诉管理员:

“这本书是3厘米厚、27厘米宽、500克。”


十八、那如果delete了错误的指针呢?

例如:

int x = 10; int *p = &x; delete p; // ❌

这是错误的。

因为:

x

不是通过:

new

申请出来的。

它可能在:

上。

delete是用来释放:

new

得到的动态内存。

所以:

new → delete

是一对。

不能:

普通变量 → delete

十九、再看一个“指针陷阱”

int *p = new int; delete p; cout << *p; // ❌

为什么?

因为:

delete p;

之后:

p ↓ 原来的内存已经归还

这时候p仍然保存着那个地址,但那个地址已经不能再当成有效对象使用。

这种指针叫:

悬空指针(dangling pointer)

比较好的习惯是:

delete p; p = nullptr;

这样:

p ↓ nullptr

至少不会继续拿旧地址访问已经释放的对象。


二十、给同学们一个“终极版模型”

把今天所有知识串起来:

new │ ↓ ┌──────────────────┐ │ operator new │ └──────────────────┘ │ ↓ 内存分配器 │ ↓ ┌─────────────────────┐ │ 管理信息 + 内存空间 │ └─────────────────────┘ │ ↓ 构造对象 │ ↓ p

然后:

delete p │ ↓ 析构对象 │ ↓ operator delete │ ↓ 内存分配器 │ ↓ 回收内存

而数组:

new T[n] ↓ 申请空间 ↓ 创建 n 个对象 ↓ 记录/管理数组所需信息 ↓ 返回 p

最后:

delete[] p ↓ 知道如何处理这组对象 ↓ 销毁对象 ↓ 释放整块内存

二十一、最后送同学们一句最重要的话

如果只记住今天的内容,请记住:

🧠指针p通常只知道“地址”,它并不负责记住整块内存有多大;真正负责记账的是内存分配器。

new负责申请内存并创建对象,delete负责销毁对象并把内存交还给分配器。

对于new[],实现机制还必须能够正确处理这一组对象,所以delete[]不能和delete混用。


这其实已经触及了一个非常重要的 C++ 底层思想:

对象生命周期 ↕ 内存生命周期

对象的“出生和死亡”与内存的“申请和释放”是两件相关但不同的事情。


返回列表