news 2026/11/2 11:19:24

C++ 内存管理:new 与 delete 详解——从语法到底层 operator new 与 malloc

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 内存管理:new 与 delete 详解——从语法到底层 operator new 与 malloc

在 C++ 中,new和delete是最常用的动态内存管理手段。但说不清它到底做了什么:为什么new会调用构造函数,而malloc不会?operator new和malloc又是什么关系?本文从语法、语义、底层调链三个层次,把这条链路彻底讲清楚。

一、C++ 的内存管理

在讲new和delete之前,先要弄清楚一个程序的内存到底被划分成了哪几块。C++ 程序运行时的内存通常可以划分为以下几个区域:栈区、堆区、全局/静态区、常量区、代码区。不同区域的生命周期、分配方式和访问速度都不一样,理解它们的差异是掌握内存管理的第一步。

1.1 内存分区总览

区域存放内容分配/释放方式生命周期
栈区(Stack)局部变量、函数参数、返回地址编译器自动分配和释放随函数调用结束而销毁
堆区(Heap)new/malloc动态申请的内存程序员手动申请和释放从申请到释放为止
全局/静态区全局变量、静态变量(static)程序启动时分配,结束时释放整个程序运行期
常量区字符串字面量、const全局常量编译期确定,只读整个程序运行期
代码区函数体的机器指令编译期确定,只读整个程序运行期
//内存管理 int globalVar = 1; //全局变量,存放在静态区 static int staticGlobalVar = 1;//存放在静态区 void Test() { static int staticVar = 1;//存放在静态区 int localVar = 1;//存放在栈 int num1[10] = { 1, 2, 3, 4 };//存放在栈 char char2[] = "abcd";//存放在栈,*char2存放在栈 const char* pChar3 = "abcd";//存放在栈,*pChar3存放在常量区 int* ptr1 = (int*)malloc(sizeof(int) * 4);//ptr1存放在栈,*ptr1 存放在堆 int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); }

1.2 从内存分区看 new 与 delete

有了上面的分区概念,再来看new和delete的完整链路就清晰了:new从堆区拿到原始内存,再在这块内存上构造对象;delete先析构对象,再把内存还给堆管理器。

flowchart TD A["new T(args)"] --> B["operator new(sizeof(T))"] B --> C["malloc(size)"] C --> D["操作系统堆 / 内存池"] B --> E["返回 void* 原始内存"] E --> F["在内存上调用 T 的构造函数"] F --> G["返回 T*"] H["delete p"] --> I["调用 T 的析构函数"] I --> J["operator delete(p)"] J --> K["free(p)"]

一句话总结:new= 分配内存(operator new → malloc)+ 构造对象;delete= 析构对象 + 释放内存(operator delete → free)。

1.3 C 语言的动态内存:malloc / calloc / realloc

C++ 的operator new底层通常就是调用malloc,所以有必要把 C 的这套动态内存函数讲清楚。它们都声明在<cstdlib>中,操作的都是堆区的原始字节内存,不涉及构造和析构。

函数原型是否初始化用途
mallocvoid* malloc(size_t size)否,内容为随机值申请指定字节数的内存
callocvoid* calloc(size_t n, size_t size)是,全部清零申请 n 个元素、每个 size 字节,并清零
reallocvoid* realloc(void* ptr, size_t size)新增部分不初始化调整已分配内存的大小

malloc:只负责分配,不初始化。返回的指针需要强制转换,失败返回nullptr。

int* p = (int*)malloc(10 * sizeof(int)); // 申请 10 个 int 的空间 if (p == NULL) { // 处理分配失败 } // 此时 p 指向的内容是未定义的 free(p); // 释放

calloc:等价于malloc之后再把内存清零,适合需要初始值为 0 的场景(如数组、哈希表)。

int* p = (int*)calloc(10, sizeof(int)); // 10 个 int,全部为 0 free(p);

realloc:用于扩大或缩小一块已分配的内存。它的行为需要特别注意:

  • 如果原内存后面有足够空间,可能原地扩展,返回原指针;
  • 如果空间不够,会分配一块新内存,把旧数据拷贝过去,释放旧内存,返回新指针;
  • 如果size为 0,行为由实现定义,可能返回nullptr或一个可释放的指针;
  • 如果失败,返回nullptr,原内存保持不变,不会丢失。
int* p = (int*)malloc(10 * sizeof(int)); int* q = (int*)realloc(p, 20 * sizeof(int)); if (q == NULL) { // 扩容失败,p 仍然有效,需要自行 free(p) free(p); } else { p = q; // 扩容成功,使用新指针 } free(p);

常见错误:直接写p = realloc(p, newSize)。一旦realloc失败返回nullptr,原来的p就被覆盖,旧内存再也无法释放,造成内存泄漏。

1.4 malloc 的实现原理

malloc并不是每次都直接向操作系统要内存,而是由 C 库在用户态维护一个堆管理器,通过空闲链表等数据结构管理已申请的内存块。其核心思路大致如下:

  1. 内存块头部:每次分配时,在返回给用户的地址前面额外存一小段元数据(块大小、是否空闲等),这就是所谓的 chunk header。
  2. 空闲链表:堆管理器把空闲块按大小组织成链表(如 glibc 的 bin 机制),分配时从合适的链表中找一块够用的内存。
  3. 分割与合并:找到的空闲块如果比请求大,就分割成“已分配部分 + 剩余空闲部分”;释放时若相邻块空闲则合并,减少内存碎片。
  4. 向操作系统申请:当空闲链表里没有合适的块时,通过brk/sbrk(小块)或mmap(大块)向内核申请新的内存区域。
// 简化示意:malloc 的核心逻辑 void* malloc(size_t size) { if (size == 0) return NULL; size = align_up(size); // 按对齐要求向上取整 Block* blk = find_free_block(size); // 在空闲链表中查找 if (blk == NULL) { blk = extend_heap(size); // 没有合适的块,向系统申请 if (blk == NULL) return NULL; } split_block(blk, size); // 分割,剩余部分放回空闲链表 blk->free = 0; return (void*)(blk + 1); // 跳过头部,返回用户可用地址 }

正因为有头部元数据和空闲链表,free只需要传入用户指针,就能通过指针偏移找到块头,从而知道要释放多大的内存。这也解释了为什么malloc和free必须严格配对,且不能释放栈上或常量区的地址。

1.5 malloc 与 new 的关系

把两者放在一起对比,就能看清 C 和 C++ 在内存管理上的分工:

  • malloc/free只管理原始字节内存,不调用构造/析构,是 C 的接口;
  • new/delete在malloc/free之上增加了构造与析构,是 C++ 的运算符;
  • 默认的operator new内部就是调用malloc,失败时转成异常;
  • 因此new分配的内存不能用free释放,malloc分配的内存也不能用delete释放。

理解了内存分区和malloc系列函数的原理,再往下看new/delete的展开过程,就会非常自然。

二、new 与 delete 的基本用法

2.1 单个对象

#include <iostream> class Widget { public: Widget() { std::cout << "Widget 构造\n"; } ~Widget() { std::cout << "Widget 析构\n"; } void run() { std::cout << "run\n"; } }; int main() { Widget* w = new Widget(); // 分配 + 构造 w->run(); delete w; // 析构 + 释放 return 0; }

2.2 数组

int* arr1 = new int[3]; // 分配 3 个对象的内存,并逐个构造 int* arr2 = new int(1); // 分配 1 个对象内存,并将其初始化为1 int* arr3 = new int[10]{1,2,3,4,5}; //前五个初始化为1-5,后面数字默认为0 delete[] arr; // 逐个析构,再释放整块内存

必须配对:new配delete,new[]配delete[]。用delete释放new[]的内存是未定义行为,在多数实现上会导致只析构第一个对象,甚至堆损坏。

对于错误匹配问题:
对于内置类型来说,使用new开空间使用free来释放,没有任何内存泄露
对于自定义类型,使用new开空间,使用free来释放,不会调用析构函数,有内存泄露的风险
对于内置类型,使用new[]来开空间,使用free释放不会有内存泄露(底层都是malloc和free)
对于自定义类型,使用new[]来开空间,使用delete来释放空间,当该类有析构函数时,编译器会在空间前面多开4字节的空间来存储到底要调用几次析构函数,此时使用delete来释放空间,前面的四个字节不会被释放,造成内存泄露,当该类没有析构函数时,delete也不会报错。

//new/delete class A { public: A(int a = 0,int b = 0) :_a(1) , _b(1) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; int _b; }; void Test2() { //注意是使用花括号 A* ptr1 = new A[2]{ { 1,1 }, { 1,2 } };//使用多参数隐式类型转换 A* ptr2 = new A[2]{ A(1, 1), A(1, 2) };//使用匿名对象 }

2.3 定位 new(placement new)

定位 new 不分配内存,只在一块已有内存上显式调用构造和析构函数,这是理解“分配与构造分离”的最好例子。

A* ptr3 = (A*)operator new(sizeof(A));//开空间 new(ptr3)A(1,2);//构造 ptr3->~A();//手动析构 operator delete(ptr3);//释放空间

三、new 到底做了什么

编译器会把new Widget()展开成大致如下的伪代码:

// 1. 分配原始内存 void* mem = operator new(sizeof(Widget)); // 2. 在内存上构造对象 Widget* w; try { w = new (mem) Widget(); // 定位 new 调用构造函数 } catch (...) { operator delete(mem); // 构造抛异常则释放内存 throw; }

关键点有三个:

  • 分配与构造分离:operator new只负责拿到一块字节内存,构造函数负责把这块内存变成对象。
  • 异常安全:如果构造函数抛异常,编译器会自动调用operator delete回收内存,不会泄漏。
  • 类型安全:new返回的是Widget*,不需要强制转换,而malloc返回void*。

new/delete 和 malloc/free 的最大区别就是对于自定义类型来说除了开空间 new/delete 会去自动调用它的构造和析构函数,内置类型无差别

四、operator new 与 operator delete

4.1 它们是什么

operator new是 C++ 标准库提供的全局函数,不是运算符重载意义上的“运算符”,它的职责只有一个:分配指定字节数的原始内存。

// 标准库中的声明(简化) void* operator new(std::size_t size); void* operator new[](std::size_t size); void operator delete(void* ptr) noexcept; void operator delete[](void* ptr) noexcept;

4.2 申请失败

默认的operator new就是malloc的一层包装,malloc 申请空间失败默认返回0,new是通过抛异常实现的,符合它面向对象的特性。

4.3 与 malloc 的对比

对比项new / deletemalloc / free
所属语言C++ 运算符C 库函数
是否调用构造/析构是否
返回类型具体类型指针void*,需强转
大小计算编译器自动计算手动传字节数
失败行为抛std::bad_alloc返回nullptr
能否重载可以重载operator new不可重载
底层实现通常调用malloc直接向堆管理器申请

五、delete 做了什么

// delete w; 大致展开为: w->~Widget(); // 1. 调用析构函数 operator delete(w); // 2. 释放内存 → free(w)

对于delete[],编译器需要知道数组长度才能逐个析构。因此new[]在分配时通常会多申请几个字节,把元素个数(cookie)存在返回地址之前,delete[]读取这个 cookie 后逆序析构所有元素,再调用operator delete[]释放整块内存。

这也是为什么new[]必须配delete[]:用delete只会析构第一个元素,且传给free的指针可能不是malloc返回的原始地址,直接导致堆损坏。

六、总结

把整条链路再回顾一遍:

  1. new T先调用operator new(sizeof(T))拿到原始内存;
  2. 默认的operator new内部调用malloc,失败时走new_handler并最终抛std::bad_alloc;
  3. 拿到内存后,编译器在内存上调用构造函数,构造失败会自动回收内存;
  4. delete先调用析构函数,再调用operator delete,最终落到free。

理解了“分配与构造分离”这一核心思想,就能明白为什么operator new可以被重载、为什么定位 new 存在、以及为什么new[]必须配delete[]。这也是从“会用 new”走向“理解 C++ 内存模型”的关键一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 11:19:05

RK3588上手笔记:1、MobaXterm连接与串口接收解析

MobaXterm 选择: 波特率:`1500000` 数据位:`8` 停止位:`1` 校验:`None` 流控:`None` 也就是:`1500000-8-N-1`。野火 LubanCat-4 的 Debug 串口默认参数就是这个。 电脑必须接 3.3V USB-TTL 串口模块,才会出现 COMx。 板子上的 USB-C OTG 烧录口不能直接作为调试串口。 …

作者头像 李华
网站建设 2026/11/2 11:15:14

Java转AI Agent教程(第四周)整理骨架与首次接触

W4 教程&#xff1a;整理骨架 第一次接触 —— 从"我学过"到"我有东西"本周交付物&#xff1a;① 一个别人能直接用的骨架仓库&#xff1b;② 一次和 AI 团队负责人的对话。 验收线&#xff08;计划表原文&#xff09;&#xff1a;骨架能三行命令跑起来&a…

作者头像 李华
网站建设 2026/11/2 11:13:49

选对生命体征监测探测器,全天候守护老人健康安全

老年人慢性病高发&#xff0c;睡眠呼吸异常、心率骤变、夜间离床走失、长时间卧床不动等隐患隐蔽性极强&#xff0c;往往在深夜悄悄发生。传统穿戴式手环、心率胸贴需要老人主动佩戴&#xff0c;不少老人排斥佩戴、忘记佩戴&#xff0c;睡觉时佩戴还存在异物感、皮肤过敏、脱落…

作者头像 李华