往期内容回顾
c++类与对象(友元)
c++类与对象(类的初始化和静态成员)
c++类与对象(赋值运算符与拷贝构造)
c++类与对象(拷贝构造)
c++类与对象(构造和析构函数)
c++类与对象(初识2)
一、 C++内存管理
在 C++ 中,new 和 delete 是用于 动态内存管理 的运算符,它们之所以存在,是为了满足 C++ 更灵活、更高效的内存控制需求,尤其在对象管理方面相对于 C 的 malloc/free 可支持自定义类对象的构造与析构
1. new 不仅分配内存,还会调用构造函数,完成对象初始化。
2. delete 不仅释放内存,还会调用析构函数,完成资源清理。
示例:链遍的构建以及内存释放
c语言实现:
使用malloc进行链表内存分配和构建,利用free进行链表的销毁。
//c语言链表的创建 --> malloc,free typedef struct ListNode_c{ int _val; struct ListNode_c* _next; struct ListNode_c* _prev; }ListNode_c; ListNode_c* Buy_NewNode(){ ListNode_c* node =(ListNode_c*)malloc(sizeof(ListNode_c)); node->_next = NULL;node->_prev = NULL; node->_val = 0; return node; } void ListDestroy(ListNode_c* phead){ assert(phead); ListNode_c* cur = phead->_next; while (cur) { ListNode_c* tmp = cur->_next; free(cur); cur = tmp; }; free(phead); phead = NULL; }
C++实现:
利用new,delete进行链表的动态内存分配,构建以及销毁。new-->调用构造函数
delete -->调用析构函数。
// c++如何使用new,delete进行动态内存管理 struct ListNode_cpp { ListNode_cpp(int val = 6) :_val(val) ,_prev(nullptr) ,_next(nullptr) {}; void ListDestroy_cpp(ListNode_cpp* phead) { ListNode_cpp* cur = phead; while (cur) { ListNode_cpp* tmp = cur->_next; delete cur; cur = tmp; } }; int _val; ListNode_cpp* _prev; ListNode_cpp* _next; };
总结
特性
malloc/free(C)
new/delete(C++)
是否调用构造函数
❌ 否
✅ 是
是否类型安全
❌ 否(返回 void* 需强转)
✅ 是(自动返回对象指针)
是否支持对象数组初始化
❌ 否
✅ 是(需配合 new[] 和 delete[])
是否能被重载
❌ 否
✅ 支持 operator new/delete 重载
容易内存泄漏
✅ 容易
✅ 仍然可能,建议配合智能指针使用
二、operator new 和 delete
new 和 delete 是 C++ 的 运算符,不仅仅是关键字,它们调用的是底层的函数:operator new 和 operator delete,你可以重载这些函数来自定义对象的内存分配方式。
operator new 和 malloc
class A{ public: int _val; }; int main(){ size_t size = 2*1024*1024*1024; A* a1 =(A*) malloc(size*sizeof(A)); cout<< a1 <<endl; // A* a2 = new A; //A* a3 = (A*) operator new(size*sizeof(A)); }
当使用malloc开辟一个很大的内存时,malloc开辟失败,则开辟的地址a1为0地址:
输出描述:
0x0并不会程序崩溃,只是开辟的0地址
当使用operator new去开辟大内存时,报错如下:
libc++abi: terminating due to uncaught exception of type std::bad_alloc: std::bad_alloc
zsh: abort "/Users/junye/Desktop/cplusplus/"memory说明你的程序因为 内存分配失败(std::bad_alloc) 而异常终止。
operator new 和 malloc 使用方式都一样,区别在于处理错误的方式不一致。
operator delete 和 free 区别在于调用析构函数清理
1、 new/delete行为拆解
new 的完整过程:
MyClass* p = new MyClass();
相当于两步操作:
调用 operator new 分配内存(只分配,不构造):
void* mem = operator new(sizeof(MyClass));
调用构造函数构造对象:
new = operator new + 构造函数MyClass* p = new (mem) MyClass();
delete的完整过程:
delete p;
相当于两步操作:
调用析构函数:
p->~MyClass();
调用 operator delete 释放内存:
operator delete(p);
delete = operator delete + 析构函数
总结:为什么我们需要operator new/ delete呢?
我们之所以需要 operator new / operator delete,是因为它们赋予了 C++ 开发者对 对象内存分配与释放的底层控制能力。相比 new 和 delete 这对高级运算符,operator new 和 operator delete 更加底层和灵活,适用于性能优化、资源控制、调试等高阶场景。
2、定位new/placement new
定位 new(placement new)是一种特殊的 new 运算符形式,允许你在指定的内存地址上构造对象,而不是从堆上分配新内存。这在自定义内存管理(如内存池、共享内存、内存对齐控制)中非常有用。
1、基本语法
void* buffer = operator new(sizeof(MyClass)); // 手动分配原始内存 MyClass* obj = new (buffer) MyClass(); // 在 buffer 上构造对象(placement new)
或常见更直观的形式:
char buffer[sizeof(MyClass)]; MyClass* obj = new (buffer) MyClass(); // 在 buffer 上构造对象
或者
class A{ public: A(){ cout<<"A()"<<endl; }; ~A(){ cout<<"~A()"<<endl; }; public: int _val; }; int main(){ // A* a1 = new A; // delete a1; A* a1 = (A*)malloc(sizeof(A)); new(a1)A(); //对已分配内存进行初始化-->定位new a1->~A(); operator delete(a1); }
调用构造函数输出:
A()
~A()
2、与普通new的对比
功能
普通 new
定位 new(placement new)
是否分配内存
✅ 是
❌ 否,需要用户自己提供内存
是否调用构造函数
✅ 是
✅ 是
是否调用 malloc
✅ 默认是
❌ 不会
是否自动释放
✅ delete 自动释放
❌ 需要手动析构 + 手动释放内存
3、 使用场景
自定义内存池(Memory Pool)
共享内存中的对象创建
提高性能:避免频繁堆分配
构造对象数组时细粒度控制生命周期
三、常见【面试题】
问题一、malloc/free/new/delete的相同点和区别
1、相同点
特征 |
说明 |
---|---|
都是 动态内存管理方式 |
都可在运行时申请内存,适合大小不确定、生命周期较长的对象或数组 |
都是 在堆上分配内存 |
内存分配来自堆区,生命周期由程序控制,不是自动释放 |
都必须 手动释放 |
需要开发者使用 free 或 delete 手动释放,否则会造成内存泄漏 |
2、区别对比:malloc/free vs new/delete
比较点 |
malloc/free(C风格) |
new/delete(C++风格) |
---|---|---|
属于语言 |
C(也可用于 C++) |
仅适用于 C++ |
返回类型 |
void*(需要强制类型转换) |
自动返回正确类型的指针 |
是否调用构造函数 |
❌ 不调用构造函数 |
✅ 自动调用构造函数 |
是否调用析构函数 |
❌ 不调用析构函数 |
✅ 自动调用析构函数 |
语法是否简洁 |
⛔ 比较繁琐:需要类型转换 |
✅ 简洁高效:无须类型转换 |
是否可重载 |
❌ 不可重载 |
✅ 可自定义 operator new/delete |
是否支持数组版本 |
❌ 需手动计算大小,例如 malloc(n * sizeof(T)) |
✅ 使用 new[] 和 delete[] |
异常处理 |
❌ 内存不足返回 NULL |
✅ 抛出 std::bad_alloc 异常(也可使用 nothrow) |
问题二、什么是内存泄露(Memory Leak)
内存泄露 是指:
程序在堆上 申请了内存,但在使用完之后 没有释放,而且也 无法再访问到 这块内存,造成内存“丢失”。
内存“还存在”,但你程序再也找不到它、也不能释放它;
久而久之,系统堆内存被“吃光”,导致程序变慢、崩溃、操作系统卡死。
2、内存泄露的分类(常见 4 类)
分类类型 |
说明 |
---|---|
🔹 1. 持续性泄露(Permanent Leak) |
程序整个生命周期都未释放,比如 new 后忘记 delete |
🔹 2. 间歇性泄露(Intermittent Leak) |
在某些条件下发生,例如多次调用某函数时,部分情况忘记释放 |
🔹 3. 假性泄露(False Leak) |
内存未释放但仍可访问,比如缓存池、单例,这些技术上不是泄露,但工具可能误报 |
🔹 4. 堆外泄露(Non-heap Leak) |
比如系统资源泄漏:文件描述符、socket、内核对象未释放(这虽然不在 heap 上,但本质类似) |
1、持续性泄露(最常见)
#include <iostream> void Leak1() { int* arr = new int[100]; // 申请内存 // 忘记 delete[] arr;,出了函数作用域后 arr 不可达 => 内存泄露 } int main() { for (int i = 0; i < 10000; ++i) { Leak1(); // 每次调用泄露 100 个 int } std::cout << "Done\n"; return 0; }
2、间歇性泄露(条件分支未覆盖)
void Leak2(bool condition) { int* p = new int(10); if (condition) { // 使用后释放 delete p; } // 如果 condition == false,就泄露了内存 }
总结:内存泄露
内容 | 举例 / 表现 |
---|---|
分类 |
持续性 / 间歇性 / 假性 / 堆外泄露 |
危害 |
性能下降、程序崩溃、信息泄露、难维护 |
典型代码 |
new 后没 delete;条件遗漏释放逻辑 |
检测工具 |
valgrind、AddressSanitizer、VLD |
预防方式 |
智能指针、RAII、代码规范、工具检查 |
问题三、为什么在32位系统下,堆无法申请4g的内存空间,而在64位下能够申请呢?
1. 地址空间只有 4GB
32 位系统的地址总线宽度为 32 位,只能表示 2³² = 4GB 的虚拟地址。
所以,一个进程的总虚拟地址空间只有 4GB。
2. 进程地址空间被
操作系统划分
在大多数操作系统中(例如 Linux/Windows),内核空间通常占用高地址部分 1GB 或 2GB。
剩下的 2~3GB 才是用户态进程可用的空间(包括堆、栈、代码段、数据段等)。
3. 堆只能用这 2~3GB 的一部分
所以你在 32 位下无法申请完整的 4GB(甚至 3GB)连续堆内存。
4.为什么 64 位可以申请远超 4GB 的堆空间
在 64 位系统下,地址空间理论上可以到 16 EB(当然受限于系统实现和硬件资源)。
操作系统会给每个进程分配极大虚拟空间(Linux 默认 128 TB 或更多)。
因为地址空间充足,不再受限于 4GB 的虚拟内存瓶颈。
只要你有足够的物理内存或 swap 资源,就能申请超大堆内存,比如几十 GB。
32 位进程地址空间(4GB):
+--------------------+ 0xFFFFFFFF (4GB)
| 内核空间(1GB) |
+--------------------+ 0xC0000000 (3GB)
| 用户空间(最多3GB)|
| 代码段 |
| 数据段 |
| 堆 ← malloc |
| ... |
| 栈 ↓ |
+--------------------+ 0x0000000064 位进程地址空间(超大):
+-----------------------------+
| 数 TB 级的用户空间 |
| 堆、映射区、栈全在中间 |
+-----------------------------+
5、总结
问题 |
原因 |
---|---|
32 位不能 malloc 4GB |
因为地址空间最大就 4GB,还需留出栈、代码段、内核空间等 |
64 位能申请大堆空间 |
因为地址空间极大,只受限于物理内存或 swap,系统资源允许即可 |