C&C++内存管理

C&C++ 内存管理笔记

为什么内存管理是 C++ 基础里的硬内容

C++ 和很多更高层语言的一个本质区别,就是它允许程序员更直接地接触对象生命周期和内存分配细节。也正因为如此,写得好时性能很强,写得不严谨时也很容易出问题。

这一课真正要解决的,不只是"记住几个函数名",而是建立下面几层意识:

  1. 程序里的对象到底分布在哪些内存区域。
  2. C 的动态内存管理和 C++ 的动态内存管理到底差在哪。
  3. new/deleteoperator new/deletemalloc/free 之间是什么关系。
  4. 为什么会有内存泄漏,怎么尽量避免。

如果这部分理解得扎实,后面学类和对象、STL、智能指针时都会顺很多。

C/C++ 程序运行时的内存分布

程序跑起来以后,内存通常不是一整块随便用,而是大致分成不同区域,每个区域承担不同职责。

常见划分可以先记成下面几类:

  1. 栈区
  2. 堆区
  3. 数据段或静态区
  4. 代码段或常量区
  5. 内存映射区

栈区里通常放什么

栈区主要存放:

  1. 非静态局部变量
  2. 函数参数
  3. 返回地址等调用相关信息

栈的特点是由系统自动管理,申请和释放速度快,但空间通常相对有限。

例如:

cpp 复制代码
void Test()
{
    int localVar = 1;
    int arr[10] = {0};
}

这里的 localVararr 本体通常都在栈上。

堆区里通常放什么

堆区主要存放运行时动态申请的内存,也就是 malloc/new 这一类接口申请出来的空间。

例如:

cpp 复制代码
int* p = new int(10);

这里:

  1. 指针变量 p 自己通常在栈上。
  2. p 指向的那个 int 对象在堆上。

堆的优点是灵活,缺点是需要程序员自己管理生命周期。

数据段和静态区放什么

全局变量、静态变量通常放在数据段。

例如:

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;

void Test()
{
    static int staticVar = 1;
}

这里的三个变量都不在栈上,而是在静态存储区。

它们的共同特点是生命周期贯穿整个程序运行过程,而不是随着函数调用结束就自动销毁。

常量区和代码段放什么

字符串常量、只读常量、程序可执行指令通常在这一类区域中。

例如:

cpp 复制代码
const char* p = "abcd";

这里:

  1. 指针变量 p 本身通常在栈上。
  2. "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);
}

可以这样理解:

  1. globalVar 在数据段。
  2. staticGlobalVar 在数据段。
  3. staticVar 在数据段。
  4. localVar 在栈区。
  5. num1 数组本体在栈区。
  6. char2 数组本体在栈区。
  7. pChar3 指针变量本身在栈区。
  8. "abcd" 常量字符串在常量区。
  9. ptr1 指针变量本身在栈区。
  10. ptr1 指向的动态空间在堆区。

这种题看起来像死记硬背,实际上是在训练你把"变量本身"和"变量指向的对象"分开理解。

sizeof 和 strlen 为什么老是一起考

因为它们很像,但本质完全不同。

sizeof 的本质

sizeof 是操作符,计算的是对象或类型占用的内存大小,单位是字节。

strlen 的本质

strlen 是库函数,统计的是字符串从起始位置到 '\0' 之前的有效字符个数。

例如:

cpp 复制代码
char arr[] = "abcd";
const char* p = "abcd";

通常可以得到:

  1. sizeof(arr) 是 5,因为包含结尾的 '\0'
  2. strlen(arr) 是 4
  3. sizeof(p) 是指针大小,64 位通常是 8
  4. strlen(p) 是 4

一句话记忆:

  1. sizeof 看的是内存占用
  2. strlen 看的是字符串长度

C 语言中的动态内存管理方式

C 语言里最常用的动态内存管理函数是:

  1. malloc
  2. calloc
  3. realloc
  4. free

malloc 的特点

malloc 按字节申请一段连续空间。

cpp 复制代码
int* p = (int*)malloc(sizeof(int));

它有几个典型特点:

  1. 只负责申请空间,不负责初始化对象语义。
  2. 返回值是 void*,在 C++ 中通常需要强转。
  3. 申请失败时返回 NULL

实际开发里,如果使用 malloc,就必须自己考虑字节数计算和返回值判空。

calloc 和 malloc 的区别

callocmalloc 的核心区别在于: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);

它可能发生两种情况:

  1. 原地扩容,返回原地址。
  2. 重新申请新空间,把旧数据拷过去,再释放旧空间。

所以 realloc 之后,原来的指针 p 不能再盲目继续用,后续应该以返回值为准。

开发里推荐写成这种风格:

cpp 复制代码
int* tmp = (int*)realloc(p, sizeof(int) * 8);
if (tmp != NULL)
{
    p = tmp;
}

因为如果 realloc 失败直接返回 NULL,原来的 p 仍然有效,不能丢。

malloc、calloc、realloc 的开发角度理解

如果从"接口能力"角度看,可以这样概括:

  1. malloc:申请一块原始空间。
  2. calloc:申请一块清零后的原始空间。
  3. realloc:尝试调整已有空间大小。

注意这里一直在强调"原始空间",因为它们不负责 C++ 对象的构造和析构语义。

C++ 为什么还要引入 new 和 delete

因为 C 的动态内存管理只解决了"空间"问题,没有真正解决"对象"问题。

在 C++ 里,一个对象通常不仅仅是一段字节,它还可能涉及:

  1. 构造函数初始化
  2. 析构函数清理资源
  3. 类型安全
  4. 异常语义

这就是 new/delete 出现的背景。

new 和 delete 管理内置类型

对于内置类型,new/deletemalloc/free 表面上看很接近,但仍然有区别。

cpp 复制代码
int* p1 = new int;
int* p2 = new int(10);
int* p3 = new int[3];

delete p1;
delete p2;
delete[] p3;

要点有两个:

  1. 单个对象用 new/delete
  2. 对象数组用 new[]/delete[]

这两组必须严格匹配,不能混着用。

new 和 delete 管理自定义类型才是真正的重点

对于自定义类型,new/deletemalloc/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;

这里本质区别是:

  1. malloc 只开空间,不会调用构造函数。
  2. free 只释放空间,不会调用析构函数。
  3. new 申请空间后会自动调用构造函数。
  4. delete 释放空间前会自动调用析构函数。

所以只要类型内部自己管理资源,直接用 malloc/free 就可能出问题。

malloc/free 和 new/delete 的区别到底怎么答

这是一个非常典型的面试题,真正答的时候建议抓住下面几点:

  1. malloc/free 是函数,new/delete 是操作符。
  2. malloc 按字节申请原始空间,new 以类型为单位申请对象空间。
  3. malloc 失败返回 NULLnew 失败默认抛异常。
  4. malloc/free 不会调用构造和析构,new/delete 会。
  5. new[]/delete[] 天然支持对象数组语义。
  6. C++ 代码里 new/delete 更符合对象生命周期管理模型。

真正好的回答,不是死背条目,而是始终围绕"空间"和"对象语义"这两个层面展开。

operator new 和 operator delete 到底是什么

这一组概念特别容易和 new/delete 混在一起。

要先分层:

  1. new/delete 是我们直接写在代码里的操作符。
  2. operator new/delete 是更底层的函数接口。

通常可以理解为:

  1. new 底层会调用 operator new 申请空间。
  2. delete 底层会调用 operator delete 释放空间。

operator new 的本质

课件里的重点结论是:operator new 本质上还是会借助 malloc 申请空间。

区别在于失败处理方式不同:

  1. malloc 失败返回 NULL
  2. operator new 失败会继续尝试应对策略,最终通常抛出 bad_alloc

所以不要把它理解成"和 malloc 毫无关系的另一套底层机制",它更像是在 C 的堆分配基础上补上了 C++ 语义。

operator delete 的本质

同理,operator delete 最终通常还是会走到 free 这一层。

所以从底层实现思路来看:

  1. operator new/delete 负责承接 C++ 的对象分配模型
  2. malloc/free 提供更原始的堆空间能力

new 和 delete 的实现原理

对内置类型

如果申请的是内置类型,new/deletemalloc/free 在"只有空间,没有复杂资源"的场景下看起来很像,但 new 仍然带着类型和异常语义。

对自定义类型

new 一个对象的过程通常可以概括成两步:

  1. 调用 operator new 申请足够大的空间。
  2. 在这块空间上调用构造函数。

delete 一个对象的过程也通常是两步:

  1. 先调用析构函数清理对象持有的资源。
  2. 再调用 operator delete 释放空间。

这也是为什么 delete 不能只是简单理解成"free 一块内存"。

new[] 和 delete[] 为什么更复杂

对象数组和单对象不一样,因为数组中每个元素都可能有独立构造和析构过程。

new T[N] 的核心过程可以理解成:

  1. 申请足够容纳 N 个对象的空间。
  2. 在这块空间上依次调用 N 次构造函数。

delete[] 的核心过程则是:

  1. 依次调用 N 次析构函数。
  2. 再释放整块空间。

所以对对象数组来说,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);

这段代码说明:

  1. malloc 先拿到一块原始内存。
  2. new(p) A(10) 在这块内存上构造对象。
  3. 用完后要显式调析构。
  4. 最后再释放原始内存。

这里最容易忘的就是"placement new 构造出来的对象,析构通常要手动调用"。

内存泄漏为什么是长期运行程序的大问题

内存泄漏不是指内存物理上消失了,而是程序失去了对某段已申请内存的控制,导致这段空间以后再也无法被正常回收和使用。

短程序里一次两次泄漏可能不明显,但对这些程序影响非常大:

  1. 后台服务
  2. 游戏服务器
  3. 长连接网关
  4. 持续运行的客户端

因为它们会越来越吃内存,最终导致响应变慢、频繁换页、甚至卡死。

常见的内存泄漏来源

申请了忘记释放

cpp 复制代码
int* p = new int;

如果作用域结束前没有 delete p,这就是最直接的堆内存泄漏。

异常路径没有清理

cpp 复制代码
int* p = new int[10];
Func(); // 如果这里抛异常,后面的 delete[] 可能执行不到
delete[] p;

这类问题在手动管理资源时代非常常见,也正是 RAII 和智能指针存在的重要背景。

系统资源泄漏

不仅堆内存会泄漏,文件句柄、socket、锁、管道等系统资源如果没及时释放,本质上也是资源泄漏。

开发里很多"程序跑久了越来越怪"的问题,根源都不一定只是内存本身。

怎么尽量减少内存泄漏

这一课虽然还没正式讲智能指针,但开发思路上已经应该有结论了:

  1. 谁申请资源,谁负责释放,责任边界要清楚。
  2. 同一个资源尽量只让一个对象负责生命周期。
  3. 尽量用对象自动管理资源,而不是依赖人工记忆。
  4. 一旦代码路径中可能抛异常,就要格外注意清理问题。

后面学 RAII、析构函数、智能指针,其实都是在系统解决这里的问题。

如何检测内存泄漏

课件里提到在 VS 下可以用 _CrtDumpMemoryLeaks() 做简单检测。

这类工具的价值在于:

  1. 发现程序退出时还有未释放资源。
  2. 帮助定位是否存在明显泄漏。

不过开发里真正排查复杂泄漏,通常还会结合更专业的工具链,例如平台调试器、运行时检测工具、日志统计等。

这一课最值得真正记住的东西

这节真正的核心不该只是"会背概念",而应该是下面这些判断力:

  1. 能分清栈、堆、静态区、常量区各自存什么。
  2. 能分清变量本身和变量指向对象所在的位置。
  3. 明白 malloc/free 管的是原始内存,new/delete 管的是对象生命周期。
  4. 知道 new/deleteoperator new/delete 是两个层级的概念。
  5. 知道 placement new 是把构造过程放到已有内存上执行。
  6. 知道内存泄漏最怕的不是一次崩掉,而是长期运行中的慢性失控。

把这些基础打牢,后面面对类、容器、智能指针和工程代码时,很多底层行为都会更容易看懂。

相关推荐
雾岛听蓝1 小时前
C文件操作与系统IO
linux·c语言·开发语言·经验分享·笔记·算法
夫唯不争,故无尤也1 小时前
HTTP方法详解:GET、POST、PUT、DELETE
开发语言·windows·python
Joker Zxc2 小时前
【前端基础(Javascript部分)】4、JavaScript的分支语句
开发语言·前端·javascript
小钻风33662 小时前
Optional:告别NullPointerException的优雅方案
开发语言·python
chools2 小时前
一篇文章带你搞懂Java“设计模式”! - - 超长文(涵盖23种)万字总结!【汇总篇】
java·开发语言·设计模式
玖釉-2 小时前
解密图形渲染的性能原罪 —— Draw Call
c++·windows·图形渲染
肆忆_2 小时前
C++ 设计模式与 SOLID 原则实战笔记
c++
肆忆_2 小时前
C++ SOLID 原则学习笔记
c++
KK_THREESTEP2 小时前
【无标题】
c++