C&C++ 内存管理笔记
为什么内存管理是 C++ 基础里的硬内容
C++ 和很多更高层语言的一个本质区别,就是它允许程序员更直接地接触对象生命周期和内存分配细节。也正因为如此,写得好时性能很强,写得不严谨时也很容易出问题。
这一课真正要解决的,不只是"记住几个函数名",而是建立下面几层意识:
- 程序里的对象到底分布在哪些内存区域。
- C 的动态内存管理和 C++ 的动态内存管理到底差在哪。
new/delete、operator new/delete、malloc/free之间是什么关系。- 为什么会有内存泄漏,怎么尽量避免。
如果这部分理解得扎实,后面学类和对象、STL、智能指针时都会顺很多。
C/C++ 程序运行时的内存分布
程序跑起来以后,内存通常不是一整块随便用,而是大致分成不同区域,每个区域承担不同职责。
常见划分可以先记成下面几类:
- 栈区
- 堆区
- 数据段或静态区
- 代码段或常量区
- 内存映射区
栈区里通常放什么
栈区主要存放:
- 非静态局部变量
- 函数参数
- 返回地址等调用相关信息
栈的特点是由系统自动管理,申请和释放速度快,但空间通常相对有限。
例如:
cpp
void Test()
{
int localVar = 1;
int arr[10] = {0};
}
这里的 localVar 和 arr 本体通常都在栈上。
堆区里通常放什么
堆区主要存放运行时动态申请的内存,也就是 malloc/new 这一类接口申请出来的空间。
例如:
cpp
int* p = new int(10);
这里:
- 指针变量
p自己通常在栈上。 p指向的那个int对象在堆上。
堆的优点是灵活,缺点是需要程序员自己管理生命周期。
数据段和静态区放什么
全局变量、静态变量通常放在数据段。
例如:
cpp
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
}
这里的三个变量都不在栈上,而是在静态存储区。
它们的共同特点是生命周期贯穿整个程序运行过程,而不是随着函数调用结束就自动销毁。
常量区和代码段放什么
字符串常量、只读常量、程序可执行指令通常在这一类区域中。
例如:
cpp
const char* p = "abcd";
这里:
- 指针变量
p本身通常在栈上。 "abcd"这个字符串常量通常在常量区。
很多初学者会把这两者混在一起,这个点一定要分清。
一段典型代码怎么分析内存分布
下面这种代码是非常经典的分析题:
cpp
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";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
}
可以这样理解:
globalVar在数据段。staticGlobalVar在数据段。staticVar在数据段。localVar在栈区。num1数组本体在栈区。char2数组本体在栈区。pChar3指针变量本身在栈区。"abcd"常量字符串在常量区。ptr1指针变量本身在栈区。ptr1指向的动态空间在堆区。
这种题看起来像死记硬背,实际上是在训练你把"变量本身"和"变量指向的对象"分开理解。
sizeof 和 strlen 为什么老是一起考
因为它们很像,但本质完全不同。
sizeof 的本质
sizeof 是操作符,计算的是对象或类型占用的内存大小,单位是字节。
strlen 的本质
strlen 是库函数,统计的是字符串从起始位置到 '\0' 之前的有效字符个数。
例如:
cpp
char arr[] = "abcd";
const char* p = "abcd";
通常可以得到:
sizeof(arr)是 5,因为包含结尾的'\0'strlen(arr)是 4sizeof(p)是指针大小,64 位通常是 8strlen(p)是 4
一句话记忆:
sizeof看的是内存占用strlen看的是字符串长度
C 语言中的动态内存管理方式
C 语言里最常用的动态内存管理函数是:
malloccallocreallocfree
malloc 的特点
malloc 按字节申请一段连续空间。
cpp
int* p = (int*)malloc(sizeof(int));
它有几个典型特点:
- 只负责申请空间,不负责初始化对象语义。
- 返回值是
void*,在 C++ 中通常需要强转。 - 申请失败时返回
NULL。
实际开发里,如果使用 malloc,就必须自己考虑字节数计算和返回值判空。
calloc 和 malloc 的区别
calloc 和 malloc 的核心区别在于:calloc 会把申请到的内存按字节清零。
cpp
int* p = (int*)calloc(4, sizeof(int));
这个例子表示申请 4 个 int 的空间,并初始化为 0。
它适合那种"默认就希望是零值"的场景,但也要注意,它的清零是字节层面的初始化,不等价于 C++ 对象构造。
realloc 为什么最容易出错
realloc 用于调整原有堆空间大小。
cpp
int* p = (int*)malloc(sizeof(int) * 4);
int* q = (int*)realloc(p, sizeof(int) * 8);
它可能发生两种情况:
- 原地扩容,返回原地址。
- 重新申请新空间,把旧数据拷过去,再释放旧空间。
所以 realloc 之后,原来的指针 p 不能再盲目继续用,后续应该以返回值为准。
开发里推荐写成这种风格:
cpp
int* tmp = (int*)realloc(p, sizeof(int) * 8);
if (tmp != NULL)
{
p = tmp;
}
因为如果 realloc 失败直接返回 NULL,原来的 p 仍然有效,不能丢。
malloc、calloc、realloc 的开发角度理解
如果从"接口能力"角度看,可以这样概括:
malloc:申请一块原始空间。calloc:申请一块清零后的原始空间。realloc:尝试调整已有空间大小。
注意这里一直在强调"原始空间",因为它们不负责 C++ 对象的构造和析构语义。
C++ 为什么还要引入 new 和 delete
因为 C 的动态内存管理只解决了"空间"问题,没有真正解决"对象"问题。
在 C++ 里,一个对象通常不仅仅是一段字节,它还可能涉及:
- 构造函数初始化
- 析构函数清理资源
- 类型安全
- 异常语义
这就是 new/delete 出现的背景。
new 和 delete 管理内置类型
对于内置类型,new/delete 和 malloc/free 表面上看很接近,但仍然有区别。
cpp
int* p1 = new int;
int* p2 = new int(10);
int* p3 = new int[3];
delete p1;
delete p2;
delete[] p3;
要点有两个:
- 单个对象用
new/delete - 对象数组用
new[]/delete[]
这两组必须严格匹配,不能混着用。
new 和 delete 管理自定义类型才是真正的重点
对于自定义类型,new/delete 和 malloc/free 的差距就非常明显了。
cpp
class A
{
public:
A(int a = 0) : _a(a) {}
~A() {}
private:
int _a;
};
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(10);
free(p1);
delete p2;
这里本质区别是:
malloc只开空间,不会调用构造函数。free只释放空间,不会调用析构函数。new申请空间后会自动调用构造函数。delete释放空间前会自动调用析构函数。
所以只要类型内部自己管理资源,直接用 malloc/free 就可能出问题。
malloc/free 和 new/delete 的区别到底怎么答
这是一个非常典型的面试题,真正答的时候建议抓住下面几点:
malloc/free是函数,new/delete是操作符。malloc按字节申请原始空间,new以类型为单位申请对象空间。malloc失败返回NULL,new失败默认抛异常。malloc/free不会调用构造和析构,new/delete会。new[]/delete[]天然支持对象数组语义。- C++ 代码里
new/delete更符合对象生命周期管理模型。
真正好的回答,不是死背条目,而是始终围绕"空间"和"对象语义"这两个层面展开。
operator new 和 operator delete 到底是什么
这一组概念特别容易和 new/delete 混在一起。
要先分层:
new/delete是我们直接写在代码里的操作符。operator new/delete是更底层的函数接口。
通常可以理解为:
new底层会调用operator new申请空间。delete底层会调用operator delete释放空间。
operator new 的本质
课件里的重点结论是:operator new 本质上还是会借助 malloc 申请空间。
区别在于失败处理方式不同:
malloc失败返回NULLoperator new失败会继续尝试应对策略,最终通常抛出bad_alloc
所以不要把它理解成"和 malloc 毫无关系的另一套底层机制",它更像是在 C 的堆分配基础上补上了 C++ 语义。
operator delete 的本质
同理,operator delete 最终通常还是会走到 free 这一层。
所以从底层实现思路来看:
operator new/delete负责承接 C++ 的对象分配模型malloc/free提供更原始的堆空间能力
new 和 delete 的实现原理
对内置类型
如果申请的是内置类型,new/delete 和 malloc/free 在"只有空间,没有复杂资源"的场景下看起来很像,但 new 仍然带着类型和异常语义。
对自定义类型
new 一个对象的过程通常可以概括成两步:
- 调用
operator new申请足够大的空间。 - 在这块空间上调用构造函数。
delete 一个对象的过程也通常是两步:
- 先调用析构函数清理对象持有的资源。
- 再调用
operator delete释放空间。
这也是为什么 delete 不能只是简单理解成"free 一块内存"。
new[] 和 delete[] 为什么更复杂
对象数组和单对象不一样,因为数组中每个元素都可能有独立构造和析构过程。
new T[N] 的核心过程可以理解成:
- 申请足够容纳
N个对象的空间。 - 在这块空间上依次调用
N次构造函数。
delete[] 的核心过程则是:
- 依次调用
N次析构函数。 - 再释放整块空间。
所以对对象数组来说,delete[] 不是可有可无的写法,而是必须精确匹配的语义。
placement new 为什么会存在
定位 new,也就是 placement new,不是重新申请内存,而是在一块已经准备好的原始内存上显式调用构造函数。
形式大致是:
cpp
new(place) T(args...);
它的核心作用不是"更高级的 new",而是把"内存分配"和"对象构造"拆开。
placement new 在什么场景真正有用
最典型的场景是内存池、对象池、自定义容器底层实现。
因为这些场景里,内存往往已经分配好了,但对象还没真正构造出来。这时就不能直接把一块原始内存当成合法对象使用,而需要显式调用构造函数。
例如:
cpp
A* p = (A*)malloc(sizeof(A));
new(p) A(10);
p->~A();
free(p);
这段代码说明:
malloc先拿到一块原始内存。new(p) A(10)在这块内存上构造对象。- 用完后要显式调析构。
- 最后再释放原始内存。
这里最容易忘的就是"placement new 构造出来的对象,析构通常要手动调用"。
内存泄漏为什么是长期运行程序的大问题
内存泄漏不是指内存物理上消失了,而是程序失去了对某段已申请内存的控制,导致这段空间以后再也无法被正常回收和使用。
短程序里一次两次泄漏可能不明显,但对这些程序影响非常大:
- 后台服务
- 游戏服务器
- 长连接网关
- 持续运行的客户端
因为它们会越来越吃内存,最终导致响应变慢、频繁换页、甚至卡死。
常见的内存泄漏来源
申请了忘记释放
cpp
int* p = new int;
如果作用域结束前没有 delete p,这就是最直接的堆内存泄漏。
异常路径没有清理
cpp
int* p = new int[10];
Func(); // 如果这里抛异常,后面的 delete[] 可能执行不到
delete[] p;
这类问题在手动管理资源时代非常常见,也正是 RAII 和智能指针存在的重要背景。
系统资源泄漏
不仅堆内存会泄漏,文件句柄、socket、锁、管道等系统资源如果没及时释放,本质上也是资源泄漏。
开发里很多"程序跑久了越来越怪"的问题,根源都不一定只是内存本身。
怎么尽量减少内存泄漏
这一课虽然还没正式讲智能指针,但开发思路上已经应该有结论了:
- 谁申请资源,谁负责释放,责任边界要清楚。
- 同一个资源尽量只让一个对象负责生命周期。
- 尽量用对象自动管理资源,而不是依赖人工记忆。
- 一旦代码路径中可能抛异常,就要格外注意清理问题。
后面学 RAII、析构函数、智能指针,其实都是在系统解决这里的问题。
如何检测内存泄漏
课件里提到在 VS 下可以用 _CrtDumpMemoryLeaks() 做简单检测。
这类工具的价值在于:
- 发现程序退出时还有未释放资源。
- 帮助定位是否存在明显泄漏。
不过开发里真正排查复杂泄漏,通常还会结合更专业的工具链,例如平台调试器、运行时检测工具、日志统计等。
这一课最值得真正记住的东西
这节真正的核心不该只是"会背概念",而应该是下面这些判断力:
- 能分清栈、堆、静态区、常量区各自存什么。
- 能分清变量本身和变量指向对象所在的位置。
- 明白
malloc/free管的是原始内存,new/delete管的是对象生命周期。 - 知道
new/delete和operator new/delete是两个层级的概念。 - 知道
placement new是把构造过程放到已有内存上执行。 - 知道内存泄漏最怕的不是一次崩掉,而是长期运行中的慢性失控。
把这些基础打牢,后面面对类、容器、智能指针和工程代码时,很多底层行为都会更容易看懂。