目录
0.1 概述&序言
一、C/C++内存分布
1.1 内存五区
1.2 一道面试题引入
二、C语言的动态内存管理
2.1 malloc 家族
三、C++的new/delete
3.1 为什么 C++ 要自己搞一套?
3.2 new/delete 操作内置类型
3.3 new/delete 操作自定义类型
四、new的底层原理:operator new
4.1 new 到底干了什么
4.2 面试扩展
4.3 小贴士:delete[] 怎么知道要调几次析构?
五、定位new表达式(placement new)
5.1 概念
六、malloc/free与new/delete的区别(面试高频)
七、内存泄漏
7.1 概念与危害
7.2 两类泄漏
7.3 如何检测与避免
十、最终总结
0.1 概述&序言
程序跑着跑着内存越占越多,最后卡死崩溃;想写个交换函数,int、double、char 各写一遍,烦不烦?本篇就解决这两大痛点:搞懂 C++ 的 new/delete 与内存管理,再学会用模板一份代码通吃所有类型。读完你就能避开内存泄漏,写出更优雅的通用代码。
一、C/C++内存分布
1.1 内存五区
先说结论:
C/C++ 程序的内存分为五大区域:栈、堆、数据段(静态区)、代码段(常量区)、内存映射段。
一句话口诀:“栈向下,堆向上,数据静态,代码常量。”
- 栈(Stack):函数参数、非静态局部变量、函数返回值等。编译器自动分配回收,向下增长(地址从高到低)。
- 堆(Heap):程序员手动申请(malloc/new)、手动释放(free/delete),向上增长。
- 数据段(静态区):全局变量、static 变量。已初始化的在 .data 段,未初始化的在 .bss 段(自动置 0)。
- 代码段(常量区):函数体(二进制指令)、字符串常量、const 修饰的全局常量。
- 内存映射段:文件映射、动态库加载、匿名映射等,了解即可。
那为什么栈向下长、堆向上长?因为栈和堆共享中间的空闲区域,相向增长才能最大化利用这段内存,避免浪费。
1.2 一道面试题引入
int globalVar = 1; // ? static int staticGlobalVar = 1; // ? int main() { static int staticVar = 1; // ? int localVar = 1; // ? int num1[10] = { 1,2,3,4 }; // ? char char2[] = "abcd"; // ? const char* pChar3 = "abcd"; // ? int* ptr1 = (int*)malloc(sizeof(int) * 4); // ? free(ptr1); return 0; }答案:
- globalVar、staticGlobalVar、staicVar:数据段(静态区)
- localVar、num1、char2:栈
- pChar3:栈(指针本身在栈上);它指向的 “abcd”:代码段(字符串常量)
- ptr1:栈(指针本身在栈上);malloc 出来的空间:堆
坑点:char2是数组,“abcd” 被拷贝到了栈上,所以char2可以修改;而pChar3指向的是代码段里的常量字符串,只读不可改——pChar3[0] = 'A';会直接崩溃(段错误)。
二、C语言的动态内存管理
2.1 malloc 家族
malloc(size):只开空间,不初始化,内容是随机的。calloc(n, size):开 n 个 size 大小的空间,并初始化为 0。realloc(ptr, size):调整空间大小(原地扩或另找新地+拷贝)。free(ptr):释放空间。
注意事项:
- malloc失败返回 NULL(不像 new 会抛异常),所以必须判空。
- free 之后建议把指针置空(
ptr = NULL;),防止野指针。 free(NULL)是安全的(空操作),可以放心判空后再 free。
三、C++的new/delete
3.1 为什么 C++ 要自己搞一套?
malloc 只能开空间。对于自定义类型,它完全不知道"构造函数"“析构函数"是什么。拿 malloc 开一个类对象的空间,这个对象就是个"死对象”——成员没初始化、资源没人收尸。
口诀:malloc 只管租地,new 负责盖楼装修(开空间 + 调构造)。
3.2 new/delete 操作内置类型
int* p1 = new int; // 开一个 int(值未初始化) int* p2 = new int(10); // 开一个 int,初始化为 10 int* p3 = new int[10]; // 开 10 个 int 的数组 int* p4 = new int[10]{ 1,2,3 }; // C++11:前三个为1,2,3,其余为0 delete p1; delete p2; delete[] p3; // 数组必须用 delete[] delete[] p4;原则:new[] 必须配对 delete[],new 配对 delete。混用属于未定义行为,会出事。
3.3 new/delete 操作自定义类型
class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } }; int main() { A* pa = new A; // 输出 A():new 会调用构造函数 delete pa; // 输出 ~A():delete 会调用析构函数 A* pb = (A*)malloc(sizeof(A)); // 只开空间,什么都不输出 free(pb); // 只释放空间,什么都不输出 return 0; }如图:这就是 new/delete 和 malloc/free 最本质的区别:new/delete 会调构造和析构,malloc/free 不会。
四、new的底层原理:operator new
4.1 new 到底干了什么
先说结论:
new = operator new(封装 malloc 开空间)+ 调用构造函数。
delete = 调用析构函数 + operator delete(封装 free 释放空间)。
注意:operator new不是new操作符,它是一个库函数,原型是void* operator new(size_t size);,底层就是对 malloc 的封装;区别在于空间申请失败时抛异常(bad_alloc),而不是返回 NULL。
所以new A翻译到底层就是:
// new A; 的等价伪代码 void* p = operator new(sizeof(A)); // 1.开空间(底层是malloc) A* pa = new(p) A; // 2.在p上调用构造函数(这就是定位new)4.2 面试扩展
operator new/operator delete是可以被重载的(内存池的常用手法,进阶篇细讲)。- 直接调用
operator new(100)等价于 malloc,返回 void*,不会调构造函数。
4.3 小贴士:delete[] 怎么知道要调几次析构?
考虑A* p = new A[10]; delete[] p;——delete[] 需要调 10 次析构,可它只拿到一个指针,怎么知道是 10 个?
先说结论:
new T[n] 在开空间时,会偷偷多开一小块空间记录对象个数(MSVC 是 4 字节,GCC 是 8 字节),delete[] 靠它知道该调多少次析构。
以MSVC x86环境下为例:
using namespace std; class A { public: A(int n = 0) :_a(n) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A* p2 = new A[10]; delete[]p1; int* p3 = new int[10]; delete[]p2; return 0; }打开其反汇编:观察p1所开空间;
除了10个类型的40个字节空间外还多开了4个字节的空间,这4个字节空间便是用来记录开了多少个对象。
这就是为什么如果用free会报错的原因,free(p2)将忽略前面开的4个字节的空间,进而从空间中间开始释放内存,这是编译器所不允许的。
内置类型p3没有析构函数,编译器通常会省掉这块记录,所以delete[]内置类型数组才显得"和 delete 没啥区别"——但这不代表可以混用,原则不变:new[] 配 delete[]。
五、定位new表达式(placement new)
5.1 概念
先说结论:
定位 new 是在已经分配好的内存上构造对象,只调构造函数、不重新开空间。
语法:new(地址) 类型(参数);
class A { public: A(int n) : _n(n) { cout << "A(" << _n << ")" << endl; } ~A() { cout << "~A()" << endl; } private: int _n; }; int main() { A* pa = (A*)operator new(sizeof(A)); // 1.手动开空间 new(pa) A(2026); // 2.在pa上定位构造 pa->~A(); // 3.手动调析构 operator delete(pa); // 4.手动释放空间 return 0; }口诀:new(地址)对象——在别人的地上盖房,房子归我管,但地皮不是我的。
定位 new 在内存池、共享内存等"空间已经有了、只差对象"的场景大有用处,了解即可。
六、malloc/free与new/delete的区别(面试高频)
先说结论:
| 维度 | malloc/free | new/delete |
|---|---|---|
| 属性 | 库函数(stdlib.h) | 操作符 |
| 参数 | 手动算字节数 | 写类型,编译器算大小 |
| 返回值 | void*,需强转 | 目标类型指针 |
| 失败 | 返回 NULL,需判空 | 抛 bad_alloc 异常 |
| 自定义类型 | 只开空间 | 开空间 + 调构造/析构 |
七、内存泄漏
7.1 概念与危害
先说结论:
内存泄漏:程序申请了内存,用完不释放(或释放不了),内存白白被占用。
危害:内存越用越少,轻则程序变卡,重则进程被系统杀死;对服务器、游戏这类长期运行的程序尤其致命。
7.2 两类泄漏
- 堆内存泄漏:malloc/new 之后忘了 free/delete,最常见。
- 系统资源泄漏:文件句柄、socket、锁等没关闭。占的不是堆内存,但同样是资源流失。
7.3 如何检测与避免
- 谁申请谁释放,成对写:写 new 之前先把 delete 写好,养成习惯。
- 使用RAII 思想:把资源交给对象管理,对象析构时自动释放——这正是智能指针做的事(下期预告!)。
- 检测工具了解即可:Linux 下的 Valgrind、VS 的诊断工具/内存泄漏检测器(进阶篇细讲)。
八、最终总结
- 内存五区:栈、堆、数据段、代码段、内存映射段;栈向下、堆向上
- malloc 只开空间;new = 开空间 + 调构造(底层是 operator new,失败抛异常)
- new[] 配 delete[](delete[] 靠隐藏计数器知道调几次析构);new 配 delete
- 定位 new 在已有空间上构造对象,不重新开空间
- 内存泄漏是"借了不还",RAII/智能指针是正道
好了本期内容就到这里,如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。