震 惊!90% 的 程 序 员 都 不 知 道 的 new / delete 底 层 黑 幕
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 C++。
💡个 人 主 页:[@笑口常开xpr 的 个 人 主 页]
(https://blog.csdn.net/2301_78847073?spm=1000.2115.3001.5343)
📚系 列 专 栏:C++ 炼 魂 场:从 青 铜 到 王 者 的 进 阶 之 路
✨代 码 趣 语:new[ ] 与 delete[ ] 是 内 存 世 界 的 “连 锁 反 应”,必 须 成 对 出 现。若 误 用 单 元 素 操 作 符,将 引 发 内 存 崩 溃 的 多 米 诺 效 应。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee
C++ 内 存 管 理 是 从 “手 动 分 配” 到 “对 象 自 动 管 理” 的 升 级。从 C 的 malloc 到 C++ 的 new,不 仅 是 语 法 变 化,更 是 对 “对 象 生 命 周 期” 的 完 整 掌 控。本 文 拆 解 new / delete 的 底 层 逻 辑、与 构 造 / 析 构 函 数 的 协 同,以 及 内 存 泄 漏 的 规 避,帮 你 夯 实 基 础。
程 序 中 的 数 据
栈 又 叫 堆 栈,存 储 非 静 态 局 部 变 量 / 函 数 参 数 / 返 回 值 等 等,栈 是 向 下 增 长 的。
内 存 映 射 段 是 高 效 的 I / O 映 射 方 式,用 于 装 载 一 个 共 享 的 动 态 内 存 库。用 户 可 使 用 系 统 接 口 创 建 共 享 共 享 内 存,做 进 程 间 通 信。
堆 用 于 程 序 运 行 时 动 态 内 存 分 配,堆 是 可 以 向 上 增 长的。
数 据 段(静 态 区) 存 储 全 局 数 据 和 静 态 数 据。
代 码 段(代 码 段) 可 执 行 的 代 码 / 只 读 常 量。
程 序 是 以 文 件 形 式 存 储 在 磁 盘 中 的。
C 语 言 中 动 态 内 存 管 理 方 式
动 态 内 存 分 配
C 语 言 是 通 过 malloc、realloc 和 calloc 来 开 辟 空 间 的。
malloc 用 来 在 内 存 的 堆 区 分 配 一 块 连 续 空 间,但 不 会 对 分 配 的 内 存 进 行 初 始 化。
calloc 用 来 分 配 指 定 类 型 和 大 小 的 空 间 并 且 会 把 分 配 的 内 存 全 部 初 始 化 为 0。
realloc 会 将 已 经 开 辟 的 空 间 大 小 调 整,也 就 是 扩 容。
C++ 内 存 管 理 方 式
C++ 兼 容 C 语 言,C 语 言 的 内 存 管 理 方 式 在 C++ 中 同 样 适 用。在 C++ 中 使 用 new 和 delete 操 作 符 来 管 理 内 存。
new 和 delete
new 运 算 符:分 配 和 初 始 化 内 存
delete 运 算 符:释 放 内 存
基 本 语 法
//分配单个对象
T* ptr = new T; //默认初始化(内置类型未初始化,类类型调用默认构造函数)
delete ptr;
T* ptr = new T(value); //值初始化(内置类型初始化为指定值,类类型调用带参构造函数)
delete ptr;
//分配数组
T* arr = new T[n]; //分配包含n个元素的数组,默认初始化
delete[ ] arr;
T* arr = new T[n](); //值初始化(C++11及以后)
delete[ ] arr;
注 意
申 请 和 释 放 单 个 元 素 的 空 间,使 用 new 和 delete 操 作 符,申 请 和 释 放 连 续 的 空 间,使 用 new[ ] 和 delete[ ] 应 该 匹 配 起 来 使 用。
代 码 示 例
#include<iostream>
using namespace std;
void Test()
{
//动态申请一个int类型的空间
//C
int* p1 = (int*)malloc(sizeof(int));
free(p1);
//C++
int* p2 = new int;
delete p2;//free(p2);
//动态申请10个int类型的空间
//C
int* p3 = (int*)malloc(sizeof(int) * 10);
free(p3);
//C++
int* p4 = new int[10];
delete[] p4;
//动态申请一个int类型的空间并初始化为10
//C
int* p5 = (int*)calloc(10, sizeof(int));
free(p5);
//C++
int* p6= new int(10);
delete p6;
}
int main()
{
Test();
return 0;
}
相 比 于 C 语 言,C++ 内 存 分 配 语 法 更 简 洁。
构 造 函 数 调 用
对 于 类 类 型,new 会 自 动 调 用 相 应 的 构 造 函 数。下 面 分 别 以 C 语 言 和 C++ 的 分 配 内 存 进 行 比 较。
C 语 言
#include<iostream>
using namespace std;
struct ListNode
{
int _val;
struct ListNode* _next;
};
struct ListNode* BuyListNode(int x)
{
//单纯的开空间
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->_next = NULL;
newnode->_val = x;
return newnode;
}
int main()
{
struct ListNode* n1 = BuyListNode(1);
struct ListNode* n2 = BuyListNode(2);
struct ListNode* n3 = BuyListNode(3);
return 0;
}
C++
#include<iostream>
using namespace std;
class ListNode
{
public:
//构造函数
ListNode(int x)
//初始化列表
: _val(x)
, _next(NULL)
//函数体
{}
private:
int _val;
struct ListNode* _next;
};
int main()
{
//开空间并调用构造函数
ListNode* nn1 = new ListNode(1);
ListNode* nn2 = new ListNode(2);
ListNode* nn3 = new ListNode(3);
return 0;
}
在 申 请 自 定 义 类 型 的 空 间 时,new 会 调 用 构 造 函 数,delete 会 调 用 析 构 函 数,而 malloc 与 free 不 会。
区 别
特性 | C 语言实现 | C++ 实现 |
---|---|---|
数据封装 | 结构体 + 外部函数 | 类 + 成员函数 |
内存分配 | malloc()+ 手动初始化 | new+ 构造函数自动初始化 |
错误处理 | 返回NULL+perror() | 抛出异常 |
类型转换 | 显式转换 | 自动类型推导 |
生命周期 | 手动管理 (分配 / 释放) | 构造函数 / 析构函数自动管理 |
代码复杂度 | 较高 (需要额外函数) | 较低 (封装在类中) |
注 意
malloc 和 free、new 和 delete 匹 配 使 用,不 能 乱 用 否 则 代 码 会 报 错。
operator new 与 operator delete 函 数
new 和 delete 分 别 是 内 存 开 辟 和 释 放 的 操 作 符,operator new 与 operator delete 是 全 局 函 数,new 在 底 层 调 用 operator new 全 局 函 数 来 申 请 空 间,delete 在 底 层 通 过 operator delete 全 局 函 数 来 释 放 空 间。
operator new
operator delete
#include<iostream>
using namespace std;
int main()
{
int* p1 = (int*)operator new(sizeof(int));
operator delete(p1);
int* p2 = new int;
delete(p2);
return 0;
}
从 上 面 的 代 码 来 看,operator new 和 operator delete 和 malloc 以 及 free 的 用 法 类 似。面 向 对 象 语 言 处 理 失 败 后 不 使 用 返 回 值,会 抛 异 常 并 捕 获 异 常,捕 获 异 常 后 会 释 放 空 间。
malloc
#include<iostream>
using namespace std;
int main()
{
int* p1 = nullptr;
do
{
p1 = (int*)malloc(1024 * 1024);//失败了返回NULL,申请的是虚拟内存
cout << p1 << endl;
} while (p1);
return 0;
}
malloc 开 辟 空 间 失 败 后 失 败 后 程 序 会 终 止,并 返 回 空 指 针。
new
#include<iostream>
using namespace std;
int main()
{
int* p1 = nullptr;
try
{
do
{
p1 = new int[1024 * 1024];
cout << p1 << endl;
} while (p1);
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
抛 出 异 常
new 开 辟 空 间 失 败 后 程 序 会 抛 出 异 常。使 用 new 会 调 用 operator new,operator new 对 malloc 进 行 了 封 装,如 果 开 辟 空 间 失 败 会 抛 出 bad allocation。开 辟 空 间 成 功 后 会 调 用 构 造 函 数。
与 new 类 似,delete 是 先 调 用 析 构 函 数 清 理 资 源,然 后 释 放 空 间,释 放 空 间 时 调 用 operator delete,然 后 调 用 free 函 数。
#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
cout << "Stack(size_t capacity = 3)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
int main()
{
//需要申请1个堆上的栈对象
Stack* p1 = new Stack;
delete p1;
return 0;
}
new 和 delete 的 实 现 原 理
内 置 类 型
如 果 申 请 的 是 内 置 类 型 的 空 间,new 和 malloc,delete 和 free 基 本 类 似,不 同 的 地 方 是:new / delete 申 请 和
释 放 的 是 单 个 元 素 的 空 间,new[ ] 和 delete[ ] 申 请 的 是 连 续 空 间,而 且 new 在 申 请 空 间 失 败 时 会 抛 异 常,malloc 会 返 回 NULL。
自 定 义 类 型
new 的 原 理
- 调 用 operator new 函 数 申 请 空 间。
- 在 申 请 的 空 间 上 执 行 构 造 函 数,完 成 对 象 的 构 造。
delete 的 原 理
- 在 空 间 上 执 行 析 构 函 数,完 成 对 象 中 资 源 的 清 理 工 作。
- 调 用 operator delete 函 数 释 放 对 象 的 空 间。
new T[N] 的 原 理
- 调 用 operator new[ ] 函 数,在 operator new[ ] 中 实 际 调 用 operator new 函 数 完 成 N 个 对 象 空 间 的 申 请
- 在 申 请 的 空 间 上 执 行 N 次 构 造 函 数
delete[ ] 的 原 理
- 在 释 放 的 对 象 空 间 上 执 行 N 次 析 构 函 数,完 成 N 个 对 象 中 资 源 的 清 理
- 调 用 operator delete[ ] 释 放 空 间,实 际 在 operator delete[ ] 中 调 用 operator delete 来 释 放 空 间
定 位 new(placement-new)表 达 式
定 义
定 位 new 表 达 式 是 在 已 分 配 的 原 始 内 存 空 间 中 调 用 构 造 函 数 初 始 化 一 个 对 象。
格 式
- new (place_address) type
- new (place_address) type(initializer-list)
place_address 必 须 是 一 个 指 针,initializer-list 是 类 型 的 初 始 化 列 表。
代 码 示 例
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
//显示调用构造函数
new(p1)A; //如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
任 何 类 型 的 指 针 都 是 内 置 类 型,内 置 类 型 不 会 自 动 调 用 构 造 函 数 和 析 构 函 数。只 有 自 定 义 类 型 才 能 自 动 调 用 构 造 函 数 和 析 构 函 数。因 为 p1 是 内 置 类 型 需 要 手 动 调 用 构 造 函 数 和 析 构 函 数。
经 典 题 目
malloc / free 和 new / delete 的 区 别
共 同 点
malloc / free 和 new / delete 都 是 从 堆 上 申 请 空 间,并 且 需 要 用 户 手 动 释 放。
不 同 点
- malloc 和 free 是 函 数,new 和 delete 是 操 作 符
- malloc 申 请 的 空 间 不 会 初 始 化,new 可 以 初 始 化
- malloc 申 请 空 间 时,需 要 手 动 计 算 空 间 大 小 并 传 递,new 只 需 在 其 后 跟 上 空 间 的 类 型 即 可,如 果 是 多 个 对 象,[ ] 中 指 定 对 象 个 数 即 可
- malloc 的 返 回 值 为 void*,在 使 用 时 必 须 强 转,new 不 需 要,因 为 new 后 跟 的 是 空 间 的 类 型
- malloc 申 请 空 间 失 败 时,返 回 的 是 NULL,因 此 使 用 时 必 须 判 空,new 不 需 要,但 是 new 需 要 捕 获 异 常
- 申 请 自 定 义 类 型 对 象 时,malloc / free 只 会 开 辟 空 间, 不 会 调 用 构 造 函 数 与 析 构 函 数,而 new 在 申 请 空 间 后 会 调 用 构 造 函 数 完 成 对 象 的 初 始 化,delete 在 释 放 空 间 前 会 调 用 析 构 函 数 完 成 空 间 中 资 源 的 清 理。
内 存 泄 漏
定 义
内 存 泄 漏 是 指 程 序 在 动 态 分 配 内 存 后,由 于 逻 辑 错 误 或 设 计 缺 陷,导 致 已 分 配 的 内 存 不 再 被 使 用,却 未 被 释 放,最 终 造 成 系 统 可 用 内 存 逐 渐 减 少 的 现 象。内 存 泄 漏 如 果 长 期 积 累,可 能 导 致 程 序 性 能 下 降、卡 顿,甚 至 崩 溃。
核 心 原 因
动 态 分 配 的 内 存 与 指 向 它 的 指 针 失 去 关 联,且 无 法 被 程 序 再 次 访 问 和 释 放。
危 害
短 期 影 响:程 序 占 用 内 存 持 续 增 长,可 能 导 致 运 行 速 度 变 慢(内 存 交 换 频 繁)。
长 期 影 响:对 于 长 时 间 运 行 的 程 序,内 存 泄 漏 会 逐 渐 耗 尽 系 统 内 存,最 终 导 致 程 序 崩 溃 或 被 操 作 系 统 强 制 终 止。
隐 蔽 性:单 次 内 存 泄 漏 可 能 微 不 足 道,但 长 期 积 累 会 引 发 严 重 问 题 且 排 查 难 度 较 高。
总 结
C++ 内 存 管 理 的 核 心 是 “智 能” - - - new / delete 结 合 构 造 / 析 构,实 现 对 象 从 创 建 到 销 毁 的 自 动 化。相 比 C 的 手 动 操 作,它 降 低 了 错 误 风 险,但 仍 需 注 意 匹 配 使 用、避 免 指 针 滥 用。掌 握 这 些,既 能 应 对 面 试,更 能 写 出 可 靠 代 码。