C++基础:(六) 内存管理深度解析:从分布到实现
目录
前言
一、内存分布:搞懂变量 “住” 在哪
1.1 示例代码与存储位置分析
1.2 内存区域划分
二、C 语言动态内存管理:malloc/calloc/realloc/free
2.1 函数功能与用法
(1)malloc:最基础的动态内存分配
(2)calloc:带初始化的动态内存分配
(3)realloc:动态调整已分配内存的大小
(4)free:释放动态内存
2.2 高频面试题:malloc/calloc/realloc 的区别
2.3 进阶面试题:glibc 中 malloc 的实现原理
三、C++ 内存管理:new 与 delete
3.1 new/delete 操作内置类型
(1)单个元素的分配与释放
(2)连续元素的分配与释放
3.2 new/delete 操作自定义类型
(1)示例代码:对比 malloc 与 new
(2)运行结果与分析
四、底层核心:operator new 与 operator delete 函数
4.1 operator new:new 的 “内存申请助手”
4.2 operator delete 函数
五、new 和 delete 的实现原理
5.1 内置类型的实现原理
5.2 自定义类型的实现原理
5.2.1 new T 的原理
5.2.2 delete p 的原理
5.2.3 new T [N] 的原理
5.2.4 delete[ ] p 的原理
5.3 示例解析
六、定位 new 表达式(placement-new)
6.1 定位 new 的语法
6.2 使用示例
6.3 使用场景
6.4 注意事项
七、malloc/free 与 new/delete 的区别
7.1 核心区别总结
总结
前言
在 C/C++ 开发中,内存管理是决定程序性能、稳定性的核心环节,也是面试高频考点。不少开发者对内存分布、动态内存分配方式的理解停留在表面,导致实际开发中频繁出现内存泄漏、野指针等问题。本文将从内存分布入手,逐步剖析 C / C++ 的内存管理方式,深入讲解 new/delete 的底层实现,对比不同管理方式的差异,帮大家构建完整的内存管理知识体系。下面就让我们正式开始吧!
一、内存分布:搞懂变量 “住” 在哪
要做好内存管理,首先得明确程序运行时内存的划分的区域。不同区域的内存,其生命周期、管理方式完全不同。我们通过一段代码结合选择题,直观理解各变量的存储位置。
1.1 示例代码与存储位置分析
先看这段包含全局变量、静态变量、局部变量、动态内存的代码:
// 全局变量
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);int* ptr2 = (int*)calloc(4, sizeof(int));int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);// 释放动态内存free(ptr1);free(ptr3);
}
1.2 内存区域划分
程序运行时,内存主要划分为栈、堆、数据段(静态区)、代码段(常量区)、内存映射段(Linux 系统中,用于共享库、进程间通信,暂不深入)。各区域的功能与变量存储规则如下:
内存区域 | 功能描述 |
---|---|
栈(Stack) | 存储非静态局部变量、函数参数、返回值;向下增长(地址从高到低分配) |
堆(Heap) | 存储程序运行时动态分配的内存;向上增长(地址从低到高分配) |
数据段(静态区) | 存储全局变量、静态变量(static 修饰);程序启动时分配,退出时释放 |
代码段(常量区) | 存储可执行代码、只读常量(如字符串常量 "abcd");只读属性 |
基于以上规则,我们来来做几道变量存储位置相关的选择题:
(选项:A. 栈 B. 堆 C. 数据段 D. 代码段)
- globalVar(全局变量)→ C
- staticGlobalVar(静态全局变量)→ C
- staticVar(静态局部变量)→ C(静态变量无论是否在函数内,均存于数据段)
- localVar(局部变量)→ A
- num1(局部数组)→ A(数组是局部变量的一种,存储在栈上)
- char2(局部字符数组)→ A(数组本身是局部变量,存于栈上)
- *char2(数组内容)→ A("abcd" 被拷贝到栈上的数组中,内容存于栈)
- pChar3(指针变量)→ A(指针是局部变量,存于栈上)
- *pChar3(指针指向的内容)→ D(指向字符串常量 "abcd",存于代码段)
- ptr1(指针变量)→ A(指针是局部变量,存于栈上)
- *ptr1(指针指向的内容)→ B(指向 malloc 分配的动态内存,存于堆上)
说明:
- 栈又称堆栈,主要用于存储非静态局部变量、函数参数以及函数返回值等数据,其内存地址分配遵循向下增长的规则,即新分配的内存地址会比之前的地址更低。
- 内存映射段是一种高效的 I/O 映射机制,核心用途是装载共享的动态内存库。此外,用户也能通过调用系统接口创建共享内存,将其作为进程间通信的媒介(若尚未学习 Linux 相关课程,当前仅需对该概念有基本了解即可)。
- 堆是程序运行过程中用于动态内存分配的区域,与栈相反,堆的内存地址分配采用向上增长的方式,新分配的内存地址会高于之前的地址。
- 数据段(也常称为静态区)的功能是存储程序中的全局数据和静态数据,这些数据的生命周期与程序运行周期一致,在程序启动时分配内存,程序退出时释放内存。
- 代码段主要存储两部分内容,一部分是程序中可执行的机器指令代码,另一部分是只读常量(如字符串常量),该区域的内容仅允许读取,不允许修改,以保证程序运行的安全性和稳定性。
二、C 语言动态内存管理:malloc/calloc/realloc/free
在C 语言中,是通过 4 个函数来实现动态内存管理的,这是 C++ 内存管理的基础。这四个函数其实我们在C语言篇时已经为大家介绍过了,这里就帮大家复习一下。
2.1 函数功能与用法
(1)malloc:最基础的动态内存分配
- 函数原型:
void* malloc(size_t size);
- 功能:从堆上申请size 字节的连续内存,不初始化内存内容(内存中是随机值)。
- 返回值:成功返回指向内存的
void*
指针,失败返回NULL
。 - 用法示例:
// 申请4个int大小的内存(int占4字节,共16字节)
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) { // 必须判空,防止malloc失败perror("malloc failed");return 1;
}
(2)calloc:带初始化的动态内存分配
- 函数原型:
void* calloc(size_t num, size_t size);
- 功能:从堆上申请num 个 size 字节的连续内存,将内存初始化为 0。
- 返回值:成功返回
void*
指针,失败返回NULL
。 - 用法示例:
// 申请4个int大小的内存,并初始化为0
int* p = (int*)calloc(4, sizeof(int));
if (p == NULL) {perror("calloc failed");return 1;
}
(3)realloc:动态调整已分配内存的大小
- 函数原型:
void* realloc(void* ptr, size_t size);
- 功能:调整
ptr
指向的动态内存大小为size 字节,分两种情况:- 原内存后有足够空间:直接在原内存后扩展,返回原指针
ptr
; - 原内存后无足够空间:重新分配一块 size 字节的内存,拷贝原内存数据,释放原内存,返回新指针。
- 原内存后有足够空间:直接在原内存后扩展,返回原指针
- 返回值:成功返回调整后的内存指针,失败返回
NULL
(原内存不会释放)。 - 用法示例:
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) return 1;// 将内存大小调整为8个int(32字节)
int* new_p = (int*)realloc(p, 8 * sizeof(int));
if (new_p == NULL) {perror("realloc failed");free(p); // 原内存未释放,需手动释放return 1;
}
p = new_p; // 更新指针,指向新内存
(4)free:释放动态内存
- 函数原型:
void free(void* ptr);
- 功能:释放
ptr
指向的动态内存(归还给堆),避免内存泄漏。 - 注意事项:
ptr
必须指向malloc/calloc/realloc
分配的内存,不能释放栈上内存;- 不能重复释放同一块内存(会导致程序崩溃);
- 释放
NULL
指针无效果(可安全调用free(NULL)
)。
- 用法示例:
int* p = (int*)malloc(4 * sizeof(int));
free(p); // 释放内存
p = NULL; // 避免野指针(释放后指针仍指向原地址,需置空)
2.2 高频面试题:malloc/calloc/realloc 的区别
这是面试中必问的基础题,其核心区别体现在初始化和功能场景上:
1. 初始化差异:
malloc
不初始化内存,分配后内存内容是随机值;calloc
会将内存初始化为 0,适合需要 “干净” 内存的场景(如数组初始化);realloc
不改变原有内存的内容(仅扩展或拷贝),新扩展的内存部分是随机值。
2. 参数与功能差异:
malloc
仅需传入 “总字节数”,功能是 “分配固定大小内存”;calloc
需传入 “元素个数” 和 “单个元素字节数”,功能是 “分配并初始化多个相同大小的元素内存”;realloc
需传入 “原内存指针” 和 “新内存总字节数”,功能是 “调整已分配内存的大小”。
3. 返回值处理差异:
- 三者失败均返回
NULL
,但realloc
失败时原内存不会释放,需手动释放原指针,避免内存泄漏。
2.3 进阶面试题:glibc 中 malloc 的实现原理
glibc 是 Linux 系统下的 C 标准库,其malloc
实现基于ptmalloc(Ptmalloc2),核心思想是 “内存池 + 空闲块管理”,避免频繁向操作系统申请内存(系统调用开销大)。其关键机制如下:
1. 内存池划分:
ptmalloc 将堆内存划分为多个 “内存池”(Arena),每个线程默认有一个 Arena,减少多线程竞争(线程安全)。
2. 空闲块管理:
空闲内存块按大小分类,用 “双向链表”(空闲链表)管理:
- 小内存块(<512 字节):按固定大小(如 8 字节、16 字节)分组,快速匹配申请需求;
- 大内存块(≥512 字节):按大小排序,通过遍历或二分查找匹配需求。
3. 内存分配策略:
- 小内存:从对应大小的空闲链表中分配,若没有则 “拆分” 大空闲块;
- 大内存:直接向操作系统申请(通过
sbrk
或mmap
系统调用),减少空闲块碎片。
4. 内存释放策略:
释放内存时,会检查相邻的空闲块,若存在则 “合并” 为大空闲块,减少碎片;若空闲块足够大,会归还给操作系统,避免内存浪费。
三、C++ 内存管理:new 与 delete
C 语言的内存管理方式在 C++ 中仍可使用,但存在明显缺陷,即无法自动调用自定义类型的构造 / 析构函数、使用繁琐(需手动计算大小、强转等)。因此 C++ 引入了new
和delete
操作符,专门用于动态内存管理。
3.1 new/delete 操作内置类型
对于 int、char 等内置类型,new
/delete
的功能与malloc
/free
类似,但用法更简洁,且失败时的处理方式不同。
(1)单个元素的分配与释放
- 语法:
类型* 指针 = new 类型(初始化值);
(初始化可选) - 释放:
delete 指针;
- 示例:
// 分配单个int,不初始化(内存为随机值)
int* p1 = new int;
// 分配单个int,初始化为10
int* p2 = new int(10);delete p1; // 释放p1
delete p2; // 释放p2
(2)连续元素的分配与释放
- 语法:
类型* 指针 = new 类型[元素个数];
(初始化可选,C++11 后支持列表初始化) - 释放:
delete[] 指针;
(必须用delete[]
,否则会内存泄漏) - 示例:
// 分配10个int,不初始化
int* p3 = new int[10];
// 分配10个int,列表初始化(C++11及以上)
int* p4 = new int[10]{1,2,3,4,5};delete[] p3; // 必须用delete[],释放连续内存
delete[] p4;
注意:
- 匹配使用:
new
对应delete(申请和释放单个元素的空间)
,new[]
对应delete[](申请和释放连续的空间)
,不匹配会导致内存泄漏或程序崩溃(尤其是自定义类型);- 初始化支持:
new
可直接初始化(如new int(10)
),malloc
需手动赋值初始化;- 失败处理:
new
失败时会抛出bad_alloc
异常(无需判空),malloc
失败返回NULL
(需判空)。
3.2 new/delete 操作自定义类型
这是new
/delete
与malloc
/free
的核心差异:对于自定义类型,new
会自动调用构造函数,delete
会自动调用析构函数,而malloc
/free
仅分配 / 释放内存,不处理构造 / 析构。
(1)示例代码:对比 malloc 与 new
#include <iostream>
using namespace std;class A {
public:// 构造函数:初始化成员变量,打印地址A(int a = 0) : _a(a) {cout << "A() 构造函数:" << this << endl;}// 析构函数:释放资源(此处无动态资源,仅打印)~A() {cout << "~A() 析构函数:" << this << endl;}
private:int _a;
};int main() {// 1. malloc/free 处理自定义类型cout << "=== malloc/free 处理自定义类型 ===" << endl;A* p1 = (A*)malloc(sizeof(A)); // 仅分配内存,不调用构造函数free(p1); // 仅释放内存,不调用析构函数p1 = NULL;// 2. new/delete 处理自定义类型cout << "\n=== new/delete 处理自定义类型 ===" << endl;A* p2 = new A(10); // 1. 分配内存;2. 调用构造函数初始化delete p2; // 1. 调用析构函数清理资源;2. 释放内存p2 = NULL;// 3. new[]/delete[] 处理多个自定义类型cout << "\n=== new[]/delete[] 处理多个自定义类型 ===" << endl;A* p3 = new A[3]; // 1. 分配3个A的内存;2. 调用3次构造函数delete[] p3; // 1. 调用3次析构函数;2. 释放内存p3 = NULL;return 0;
}
(2)运行结果与分析
运行结果如下,我们可清晰看到new
/delete
对构造 / 析构的调用:
=== malloc/free 处理自定义类型 ===
(无构造/析构打印,仅分配释放内存)=== new/delete 处理自定义类型 ===
A() 构造函数:0x55f8d7a7a280
~A() 析构函数:0x55f8d7a7a280=== new[]/delete[] 处理多个自定义类型 ===
A() 构造函数:0x55f8d7a7a290
A() 构造函数:0x55f8d7a7a294
A() 构造函数:0x55f8d7a7a298
~A() 析构函数:0x55f8d7a7a298
~A() 析构函数:0x55f8d7a7a294
~A() 析构函数:0x55f8d7a7a290
分析如下:
malloc
分配A
类型内存时,无构造函数调用,p1
指向的内存仅是 “一块大小为sizeof(A)
的空间”,不能算完整的A
对象;new A(10)
会先分配内存,再调用构造函数初始化,p2
指向的是完整的A
对象;delete p2
会先调用析构函数(若A
有动态资源,如new
的内存,会在此处释放),再释放内存,避免资源泄漏;new[]
/delete[]
会调用多次构造 / 析构(个数等于元素个数),确保每个对象都被正确初始化和清理。
四、底层核心:operator new 与 operator delete 函数
很多人会误以为new
/delete
是函数,但实际它们是操作符。new
在底层会调用operator new
函数申请内存,delete
会调用operator delete
函数释放内存。这两个函数是系统提供的全局函数,我们可以查看其源码(基于 MSVC 或 glibc)理解实现逻辑。
4.1 operator new:new 的 “内存申请助手”
operator new
的核心功能是 “从堆上申请内存”,底层依赖malloc
实现,同时处理内存申请失败的场景(抛异常)。下面是 MSVC 编译器中的简化实现:
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {// 尝试分配size字节的内存void* p;while ((p = malloc(size)) == 0) {// 如果malloc失败,尝试调用用户设置的内存不足处理函数if (_callnewh(size) == 0) {// 若没有有效的处理函数,则抛出bad_alloc异常static const std::bad_alloc nomem;_RAISE(nomem);}}return p;
}
- 如果
malloc
申请成功,直接返回内存指针;- 如果
malloc
申请失败,会尝试调用用户设置的内存不足处理函数;- 如果没有有效的处理函数,就抛出
std::bad_alloc
异常。
4.2 operator delete 函数
operator delete
是系统提供的全局函数,用于释放内存,其底层依赖于free
函数。下面是 MSVC 编译器中的简化实现:
void operator delete(void* pUserData) {_CrtMemBlockHeader* pHead;if (pUserData == NULL)return;// 线程同步,防止多线程冲突_mlock(_HEAP_LOCK);__TRY// 获取内存块头部信息pHead = pHdr(pUserData);// 验证内存块类型_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));// 释放内存(最终调用free)_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK);__END_TRY_FINALLYreturn;
}// free的宏定义,实际调用_free_dbg
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator delete
底层是通过free
来释放内存,同时还增加了一些调试和线程同步的处理:
- 检查指针是否为
NULL
,如果是则直接返回;- 进行线程同步,确保多线程环境下的安全性;
- 验证内存块的有效性,用于调试;
- 最终通过
free
释放内存。
五、new 和 delete 的实现原理
new
和delete
的实现原理因操作的类型(内置类型和自定义类型)而有所不同。
5.1 内置类型的实现原理
对于内置类型(如 int、char 等),new
和delete
的实现相对简单:
new T
:调用operator new(sizeof(T))
申请内存,返回指向该内存的指针;delete p
:调用operator delete(p)
释放内存;new T[N]
:调用operator new[](N * sizeof(T))
申请内存,返回指向该内存的指针;delete[] p
:调用operator delete[](p)
释放内存。
内置类型的new
/delete
与malloc
/free
的主要区别在于:
new
失败时抛出异常,malloc
返回NULL;
new
不需要手动计算内存大小和强转类型。
5.2 自定义类型的实现原理
对于自定义类型,new
和delete
除了申请和释放内存外,还会调用构造函数和析构函数。
5.2.1 new T 的原理
- 调用
operator new(sizeof(T))
申请内存空间 - 在申请到的内存空间上调用构造函数,完成对象的初始化
new T → operator new(sizeof(T)) → 构造函数
5.2.2 delete p 的原理
- 在
p
指向的内存空间上调用析构函数,完成对象中资源的清理; - 调用
operator delete(p)
释放内存空间。
delete p → 析构函数 → operator delete(p)
5.2.3 new T [N] 的原理
- 调用
operator new[](N * sizeof(T))
申请内存空间(实际会多申请一些空间存储对象个数); - 在申请到的内存空间上调用 N 次构造函数,初始化每个对象。
new T[N] → operator new[](N * sizeof(T)) → N次构造函数
5.2.4 delete[ ] p 的原理
- 在
p
指向的内存空间上调用 N 次析构函数,清理每个对象的资源; - 调用
operator delete[](p)
释放内存空间。
delete[] p → N次析构函数 → operator delete[](p)
5.3 示例解析
下面我们以自定义类型A
为例,分析new
和delete
的执行过程:
// new A(10)的执行过程:
1. 调用operator new(sizeof(A))申请内存
2. 调用A的构造函数A(10)初始化对象// delete p的执行过程:
1. 调用p指向对象的析构函数~A()
2. 调用operator delete(p)释放内存// new A[3]的执行过程:
1. 调用operator new[](3 * sizeof(A))申请内存
2. 依次调用3次A的构造函数初始化每个对象// delete[] p的执行过程:
1. 依次调用3次A的析构函数清理每个对象
2. 调用operator delete[](p)释放内存
六、定位 new 表达式(placement-new)
定位 new 表达式允许在已分配的内存空间上调用构造函数初始化对象,主要用于内存池等场景。
6.1 定位 new 的语法
// 在指定地址上构造对象,无参数
new (place_address) type;// 在指定地址上构造对象,带参数
new (place_address) type(initializer-list);
其中,place_address
是一个指向已分配内存的指针,initializer-list
是构造函数的参数列表。
6.2 使用示例
#include <iostream>
using namespace std;class A {
public:A(int a = 0) : _a(a) {cout << "A() 构造函数:" << this << ", _a = " << _a << endl;}~A() {cout << "~A() 析构函数:" << this << endl;}private:int _a;
};int main() {// 1. 分配一块与A对象大小相同的内存(未初始化)A* p = (A*)malloc(sizeof(A));if (p == nullptr) {perror("malloc failed");return 1;}// 2. 使用定位new在已分配的内存上构造A对象new(p) A(10); // 调用A的构造函数初始化p指向的内存// 3. 使用对象// ...// 4. 手动调用析构函数(定位new不会自动调用析构函数)p->~A();// 5. 释放内存free(p);p = nullptr;return 0;
}
运行结果如下:
A() 构造函数:0x55f8d7a7a280, _a = 10
~A() 析构函数:0x55f8d7a7a280
6.3 使用场景
定位 new 主要用于内存池技术:
- 内存池预先分配一块大的内存空间;
- 当需要创建对象时,从内存池中分配一块内存;
- 使用定位 new 在分配的内存上构造对象;
- 释放对象时,先手动调用析构函数,再将内存归还给内存池。
这种方式可以减少内存分配的开销,提高程序性能,尤其适用于频繁创建和销毁对象的场景。
6.4 注意事项
- 定位 new 只是调用构造函数初始化已分配的内存,不会分配新的内存;
- 使用定位 new 构造的对象,需要手动调用析构函数(
p->~A()
); - 手动调用析构函数后,还需要释放内存(如
free(p)
)。
七、malloc/free 与 new/delete 的区别
malloc
/free
和new
/delete
都是用于动态内存管理的工具,它们的主要区别如下:
特性 | malloc/free | new/delete |
---|---|---|
性质 | 函数 | 操作符 |
初始化 | 不初始化内存 | 可以初始化(如new int(10) ) |
内存大小 | 需要手动计算字节数 | 只需指定类型,自动计算大小 |
返回值 | 返回void* ,需要强转 | 返回对应类型的指针,无需强转 |
失败处理 | 返回NULL ,需手动判空 | 抛出bad_alloc 异常 |
自定义类型 | 只分配 / 释放内存,不调用构造 / 析构函数 | 分配内存后调用构造函数,释放内存前调用析构函数 |
数组处理 | 需要手动计算总大小 | 使用new[] 和delete[] ,自动处理数组 |
7.1 核心区别总结
-
对自定义类型的处理:这是两者最核心的区别。
new
/delete
会自动调用构造函数和析构函数,而malloc
/free
不会。对于包含动态资源的类,这一点是至关重要的,它确保资源能够正确初始化和释放。 -
使用便捷性:
new
不需要手动计算内存大小和强转类型,使用起来比malloc
更简洁。 -
错误处理方式:
malloc
通过返回NULL
表示失败,需要手动检查;new
通过抛出异常表示失败,需要使用try-catch
处理。 -
扩展性:
operator new
和operator delete
可以被重载,允许自定义内存分配策略;而malloc
和free
不能被重载。
总结
内存管理是 C/C++ 编程中的核心技能,本文从内存分布、C 语言内存管理、C++ 内存管理、底层实现原理等方面进行了全面解析。掌握内存管理不仅能帮助我们写出更高效、更稳定的代码,也是深入理解 C/C++ 语言特性的关键。下期博客我将为大家介绍C++模版的相关内容,请大家多多关注!