内存管理
- [1. C/C++内存分布](#1. C/C++内存分布)
-
- 进程地址空间各区域功能说明
- [1. 代码段(Code Segment / 文本段 / 常量区)](#1. 代码段(Code Segment / 文本段 / 常量区))
- [2. 数据段(Data Segment / 静态区)](#2. 数据段(Data Segment / 静态区))
- [3. 堆(Heap)](#3. 堆(Heap))
- [4. 代码区(Code Segment)](#4. 代码区(Code Segment))
- [5. 栈(Stack / 堆栈)](#5. 栈(Stack / 堆栈))
- [2. C语言中动态内存管理方式:malloc/calloc/realloc/free](#2. C语言中动态内存管理方式:malloc/calloc/realloc/free)
-
- 示例代码
- [1. malloc/calloc/realloc 的区别](#1. malloc/calloc/realloc 的区别)
- [2. malloc 的实现原理](#2. malloc 的实现原理)
- [3. C++内存管理方式](#3. C++内存管理方式)
- [4. operator new 与 operator delete 函数](#4. operator new 与 operator delete 函数)
- [5. new和delete的实现原理](#5. new和delete的实现原理)
-
- [5.1 内置类型](#5.1 内置类型)
- [5.2 自定义类型](#5.2 自定义类型)
-
- new的实现原理
- delete的实现原理
- [new T[N]的实现原理](#new T[N]的实现原理)
- delete[]的实现原理
- 核心代码实现与原理模拟
- 关键补充知识点
- [7. 内存泄漏](#7. 内存泄漏)
-
- [7.1 什么是内存泄漏 内存泄漏的危害](#7.1 什么是内存泄漏 内存泄漏的危害)
1. C/C++内存分布
进程地址空间各区域功能说明
进程地址空间是操作系统为每个进程划分的虚拟内存地址范围,并非物理内存的直接映射。不同区域有严格的功能划分和访问规则,从低地址到高地址依次为:

1. 代码段(Code Segment / 文本段 / 常量区)
- 核心功能:存储程序中可执行的机器指令(编译后的二进制代码)、只读常量(如字符串常量、const修饰的只读全局常量等)。
- 访问权限:只读(READ-ONLY),禁止写入操作,防止程序指令被篡改,提升安全性。
- 存储内容示例 :
- 函数的二进制执行代码(如Test()函数的指令)。
- 字符串常量(如代码
char* c = "abcd"中的"abcd")。 - const修饰的全局只读常量。
- 特性 :
- 多个进程可共享同一份代码段(如系统库函数的代码),节省内存。
- 程序运行期间大小固定,不会动态变化。
2. 数据段(Data Segment / 静态区)
- 核心功能 :存储全局数据、静态数据,分为初始化数据段和未初始化数据段。
- 初始化数据段:存储已初始化的全局变量、静态变量(如globalVar、staticGlobalVar、staticVar)。
- 未初始化数据段:存储未初始化的全局变量、静态变量,程序启动时操作系统会自动将其初始化为0。
- 访问权限:可读可写(READ-WRITE),运行期间可修改变量值。
- 特性 :
- 生命周期与进程一致,程序启动时分配内存,程序退出时释放。
- 大小在编译期确定,运行期间不会动态扩展/收缩。
- 全局变量和静态变量默认存储在此区域,无需手动分配/释放。
3. 堆(Heap)
- 核心功能:程序运行时动态内存分配的核心区域,用于存储运行期按需创建的数据。
- 访问权限:可读可写(READ-WRITE)。
- 特性 :
- 地址空间向上增长(从低地址向高地址扩展如上面的图片中的样子)。
- 生命周期由程序员控制,需手动通过malloc/calloc/realloc申请,free释放,若未释放会导致内存泄漏。
- 大小不固定,可随动态内存分配/释放灵活变化。
- 堆的管理由操作系统的内存管理器完成,频繁申请/释放会产生内存碎片。
4. 代码区(Code Segment)
- 核心功能 :
- 存储程序编译后的可执行机器指令(如函数的二进制执行代码),是CPU能直接解析运行的核心内容。
- 存储程序中的只读常量(如字符串常量、const修饰的全局只读常量),保障常量不被篡改。
- 作为程序运行的基础载体,所有进程的执行逻辑均依赖代码区的指令序列。
- 访问权限:默认只读(READ-ONLY),禁止写入操作,防止程序指令或只读常量被恶意/误操作修改,提升程序运行安全性。
- 特性 :
- 是进程地址空间的基础核心区域,程序启动时优先加载,退出时最后释放。
- 多个进程可共享同一份代码区内容(如系统标准库函数的指令),大幅节省内存资源。
- 大小在编译期确定,程序运行期间固定不变,无动态扩展/收缩可能。
5. 栈(Stack / 堆栈)
- 核心功能:存储非静态局部变量、函数参数、函数返回值、函数调用的上下文信息(如返回地址、ebp寄存器值等)。
- 访问权限:可读可写(READ-WRITE)。
- 特性 :
- 地址空间向下增长(从高地址向低地址扩展如图所示)。
- 由操作系统自动管理,函数调用时分配栈帧,函数返回时释放栈帧,无需程序员干预。
- 栈的大小固定(通常为几MB),超出会触发栈溢出(Stack Overflow)会直接结束程序。
- 存储的变量生命周期为函数调用周期,函数执行结束后变量立即销毁。
- 栈帧是栈的基本单位,每个函数调用对应一个独立的栈帧,保护函数调用的独立性。
示例代码
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);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
假设不同分区为以下所示:
A. 栈
B. 堆
C. 数据段 (静态区)
D. 代码段 (常量区)
那么代码中的不同变量所在的分区为表中所示
| 变量 | 位置 | 详细解析 |
|---|---|---|
| globalVar | C | globalVar 是全局初始化变量,存储在数据段(.data 区)。 |
| staticGlobalVar | C | staticGlobalVar 是全局静态初始化变量,存储在数据段(.data 区)。 |
| staticVar | C | staticVar 是局部静态初始化变量,存储在数据段(.data 区)。 |
| localVar | A | localVar 是局部非静态变量,存储在栈区(Test 函数的栈帧中)。 |
| num1 | A | num1 是局部非静态数组,存储在栈区,数组元素均在栈帧内。 |
| char2 | A | char2 是局部非静态字符数组,存储在栈区。 |
| *char2 | A | *char2 是 char2 数组首元素的解引用,数组本身在栈区,因此指向栈区的数组元素('a')。 |
| pChar3 | A | pChar3 是局部非静态指针变量,存储在栈区。 |
| *pChar3 | D | *pChar3 指向字符串常量 "abcd",该常量存储在代码段(只读常量区)。 |
| ptr1 | A | ptr1 是局部非静态指针变量,存储在栈区。 |
| *ptr1 | B | *ptr1 是 ptr1 指向的动态内存,由 malloc 分配,存储在堆区。 |
衍生问题
| 问题 | 答案 | 详细解析 |
|---|---|---|
| sizeof(num1) | 40 | num1 是 int 类型数组,包含 10 个元素,int 占 4 字节,10*4=40;sizeof 计算数组总字节数,与数组元素初始化与否无关。 |
| sizeof(char2) | 5 | char2 是字符数组,存储 "abcd" 时编译器自动追加 '\0' 作为字符串结束符,共 5 个 char 元素,char 占 1 字节,总字节数为 5。 |
| strlen(char2) | 4 | strlen 是库函数,仅统计 '\0' 之前的有效字符数,"abcd" 共 4 个字符,因此结果为 4。 |
| sizeof(pChar3) | 4/8 | pChar3 是指针变量,32 位系统下指针占 4 字节,64 位系统下指针占 8 字节;sizeof 计算指针本身的字节数,与指向的内容无关。 |
| strlen(pChar3) | 4 | pChar3 指向代码段的字符串常量 "abcd",strlen 统计 '\0' 前字符数,结果为 4。 |
| sizeof(ptr1) | 4/8 | ptr1 是指针变量,32 位系统占 4 字节,64 位系统占 8 字节;所有指针类型的 sizeof 结果仅与系统位数相关。 |
sizeof 和 strlen 核心区别说明
- 本质属性 :
- sizeof:C/C++ 的内置运算符,编译期间完成计算,不涉及运行时逻辑。
- strlen:C 标准库函数(<string.h>),运行期间遍历字符串,直到遇到 '\0' 停止计算。
- 计算范围 :
- sizeof:计算变量 / 数据类型占用的总字节数,包含 '\0'、数组未初始化部分等所有内存。
- strlen:仅统计字符串中 '\0' 之前的有效字符个数,不包含 '\0',也不处理非字符串数据。
- 适用场景 :
- sizeof:可用于任意数据类型(int、char、数组、指针、结构体、类等),无格式要求。
- strlen:仅适用于以 '\0' 结尾的字符串(char*),对非字符串类型(如 int 数组)使用会导致未定义行为(越界访问)。
- 参数要求 :
- sizeof:参数可以是类型名(如 sizeof (int))、变量名(如 sizeof (num1))、表达式(如 sizeof (1+2))。
- strlen:参数必须是指向以 '\0' 结尾的字符串的指针(char*),不能直接传入类型名。
- 返回值意义 :
- sizeof:返回值表示内存占用的字节数,反映数据的存储大小。
- strlen:返回值表示字符串的有效长度,反映字符串的逻辑长度。### 1. C/C++内存分布
2. C语言中动态内存管理方式:malloc/calloc/realloc/free
示例代码
cpp
void Test (){
// malloc申请内存 未初始化
int* p1 = (int*) malloc(sizeof(int));
free(p1); // 释放malloc申请的内存
// calloc申请内存 自动初始化
int* p2 = (int*)calloc(4, sizeof (int));
// realloc调整p2指向的内存大小
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 无需free(p2) realloc成功后p2已失效
free(p3 ); // 释放realloc调整后的内存
}
1. malloc/calloc/realloc 的区别
- 核心区别总结 :
- malloc :
- 功能:向堆区申请指定字节数的连续内存空间。
- 初始化:申请的内存未初始化,内容为随机值。
- 参数:仅需传入申请的字节数(例如:
malloc(sizeof(int)*4))。
- calloc :
- 功能:向堆区申请指定数量 + 指定大小的连续内存空间。
- 初始化:申请的内存自动初始化为 0。
- 参数:需传入元素个数和单个元素大小(例如:
calloc(4, sizeof(int)))。 - 等价于:
malloc(4*sizeof(int))+memset(申请的内存, 0, 4*sizeof(int))。
- realloc :
- 功能:调整已申请动态内存的大小(扩大或缩小)。
- 内存处理:
- 若原内存后有足够连续空间,直接扩容,返回原地址。
- 若原内存后无足够空间,重新申请新内存,拷贝原数据,释放原内存,返回新地址。
- 参数:需传入原内存指针和新的总字节数(例如:
realloc(p2, sizeof(int)*10))。 - 注意:成功调用后原指针失效,无需释放原指针,仅释放新返回的指针。
- malloc :
- 关键注意点 :
realloc调整内存后,原指针(如示例中的p2)无需调用free,否则会导致重复释放。- 三者申请的内存均在堆区,需手动调用
free释放,否则会造成内存泄漏。
2. malloc 的实现原理
- 核心原理(简要说明) :
- 底层依赖 :基于操作系统的内存管理接口(如 Linux 的
brk/sbrk/mmap)实现。 - 内存池机制 :
malloc并非每次申请都直接向操作系统要内存,而是先向系统申请一块大内存作为内存池。后续小内存申请直接从内存池分配,减少系统调用开销。 - 空闲链表管理:用链表管理已释放的空闲内存块,记录块的大小和地址。申请内存时遍历空闲链表,按适配策略(如首次适配或最佳适配)找到合适的内存块分配。
- 内存对齐:分配的内存会按系统要求对齐(如 8 字节或 16 字节对齐),保证内存访问效率。
- 扩容机制:若内存池不足,会再次向操作系统申请新的大块内存,补充内存池。
- 底层依赖 :基于操作系统的内存管理接口(如 Linux 的
3. C++内存管理方式
C语言内存管理方式在C++中可继续使用,但在自定义类型场景下存在局限且使用繁琐,因此C++提出专属内存管理方式,通过new和delete操作符实现动态内存管理。
3.1 new/delete操作内置类型
核心用法对比
| 实现方式 | 代码示例 | 核心特点 |
|---|---|---|
| C语言(malloc/free) | int* p1 = (int*)malloc(sizeof(int)); int* p3 = (int*)malloc(sizeof(int)*10); free(p1); free(p3); |
malloc是函数,需手动强转类型,仅完成内存申请/释放 |
| C++(new/delete) | int* p2 = new int; int* p2_init = new int(10); int* p4 = new int[10]; delete p2; delete[] p4; |
new是操作符,无需强转类型,支持单个元素初始化 |
核心知识点
- 申请释放单个元素空间,必须匹配使用
new和delete。 - 申请释放连续空间,必须匹配使用
new[]和delete[]。 - 内置类型数组无法通过
new int[10](10)方式统一初始化。 - 内置类型场景下,
new/delete仅简化语法,功能等价于malloc/free。
3.2 new和delete操作自定义类型
cpp
class A{
public:
A()
{
_a = 0;
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void Test2()
{
A* p1 = (A*)malloc(sizeof(A));
A* p4 = new A;
free(p1);
delete p4;
}
核心知识点
- 内置类型场景,
new/delete与malloc/free功能效果一致。 - 自定义类型场景,核心差异:
malloc仅申请内存空间,不调用构造函数。free仅释放内存空间,不调用析构函数。new在申请空间后自动调用构造函数,完成对象初始化。delete在释放空间前自动调用析构函数,完成对象资源清理。
- C++中优先使用
new/delete管理自定义类型内存。
4. operator new 与 operator delete 函数
4.1 operator new 与 operator delete 函数(重点)
核心概念
new/delete是用户层的内存申请/释放操作符。operator new/operator delete是系统提供的全局函数,并非运算符重载。new底层调用operator new申请空间,delete底层调用operator delete释放空间。operator new用法与malloc一致。
核心代码实现
cpp
void Test3()
{
size_t size = 2;
void* p3 = malloc(size * 1024 * 1024 * 1024);
cout << p3 << endl;
free(p3);
try
{
void* p4 = operator new(size * 1024 * 1024 * 1024);
cout << p4 << endl;
operator delete (p4);
}
catch(exception& e)
{
cout << e.what() << endl;
}
}
核心知识点
- 内存大小计算需使用无符号整形(
size_t):有符号整形范围为 − 2 31 ∼ 2 31 − 1 -2^{31} \sim 2^{31}-1 −231∼231−1, 2 × 1024 × 1024 × 1024 = 2 31 2 \times 1024 \times 1024 \times 1024 = 2^{31} 2×1024×1024×1024=231,超出范围会导致溢出。 malloc与operator new的核心区别:malloc申请空间失败返回NULL。operator new申请空间失败抛出异常,异常是面向对象处理错误的标准方式。
operator new底层实现逻辑:调用malloc申请内存空间,申请成功直接返回,申请失败则执行用户提供的空间不足应对措施,无应对措施则抛异常。operator delete底层实现逻辑:最终通过free释放空间,释放失败直接终止进程。- 设计
operator delete仅为与operator new成对出现,功能与free无区别。 new的完整逻辑:operator new申请空间 + 调用构造函数初始化。delete的完整逻辑:调用析构函数清理资源 +operator delete释放空间。new相比malloc的核心优势:自动调用构造函数初始化、申请失败抛异常。delete相比free的核心优势:自动调用析构函数清理资源。
5. new和delete的实现原理
5.1 内置类型
核心知识点
- 内置类型场景下,new/malloc、delete/free 功能基本一致。
- 核心差异点:
- 单个元素空间使用 new/delete,连续空间使用 new[]/delete[],需严格匹配。
- new 申请空间失败时抛出异常,malloc 申请失败返回 NULL。
5.2 自定义类型
new的实现原理
- 调用 operator new 函数完成内存空间申请。
- 在申请到的内存空间上执行构造函数,完成对象初始化。
delete的实现原理
- 在目标内存空间上执行析构函数,完成对象内资源的清理。
- 调用 operator delete 函数释放该内存空间。
new T[N]的实现原理
- 调用 operator new[] 函数,该函数内部实际调用 operator new 函数,完成 N 个对象所需连续内存空间的申请。
- 在申请到的连续空间上依次执行 N 次构造函数,完成 N 个对象的初始化。
delete[]的实现原理
- 在待释放的连续内存空间上依次执行 N 次析构函数,完成 N 个对象的资源清理。
- 调用 operator delete[] 函数释放空间,该函数内部实际调用 operator delete 函数完成内存释放。
核心代码实现与原理模拟
cpp
class B {
public:
B(int b = 0)
: _b(b) {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
private:
int _b;
};
void Test4() {
// 标准 new/delete 使用方式
B* p1 = new B;
delete p1;
// 手动模拟 new 的实现过程
// 步骤 1:调用 operator new 申请内存空间
B* p2 = (B*)operator new(sizeof(B));
// 步骤 2:使用定位 new 在已申请空间上执行构造函数初始化
// 语法:new(内存空间指针)类名(构造函数参数)
new(p2) B(1);
// 手动模拟 delete 的实现过程
// 步骤 1:显式调用析构函数清理资源(析构函数公有可显式调用,构造函数不可)
p2->~B();
// 步骤 2:调用 operator delete 释放内存空间
operator delete(p2);
}
关键补充知识点
- 定位 new(placement new):用于在已申请的内存空间上显式调用构造函数完成对象初始化,语法为 new(空间指针)类型(构造参数)。
- 构造函数不可显式调用,仅能由编译器自动调用或通过定位 new 间接调用。
- 析构函数为公有属性时可显式调用,用于手动触发资源清理。
- 堆区申请 4G 大小空间的前提:编译器需调整为 64 位,32 位系统地址空间不足无法支持。
7. 内存泄漏
7.1 什么是内存泄漏 内存泄漏的危害
- 内存泄漏的定义:内存泄漏是指因程序设计疏忽或错误,导致已分配且不再使用的内存未能被释放的情况。内存泄漏并非物理内存消失,而是程序失去对该段内存的控制权,造成内存资源的浪费。
- 内存泄漏的危害:长期运行的程序受内存泄漏影响极大,如操作系统、后台服务、服务器程序等。泄漏的内存会持续占用系统资源,导致可用内存逐渐减少,程序响应速度越来越慢。严重时会耗尽系统所有可用内存,造成程序卡死、系统崩溃。
- 典型场景示例:
- 服务器后台服务:电商平台的订单处理服务,每处理一个订单就泄漏 1KB 内存,单日百万级订单量会导致泄漏约 1GB 内存,运行一周后内存耗尽,服务无响应,订单无法处理。
- 操作系统组件:系统桌面进程存在内存泄漏,每打开/关闭一个文件就泄漏少量内存,长期使用后系统可用内存不足,桌面卡顿、操作延迟,最终需重启系统。
- 嵌入式设备程序:智能家居的控制芯片程序,因内存泄漏持续占用有限的嵌入式内存,运行数月后内存耗尽,设备失去响应,无法执行开关灯、调节温度等指令。