C++内存管理:从变量储存到new/delete 底层原理
😏https://share.note.youdao.com/s/2Tnw5HvW😏【笔记链接】
在 C/C++ 编程中,内存管理是绕不开的核心话题,它直接影响程序的性能、稳定性甚至安全性。很多开发者在初学时容易混淆不同变量的存储位置,也对malloc/free与new/delete的区别一知半解。本文将从内存区域划分、变量存储位置入手,逐步深入new/delete的底层实现,帮你彻底理清 C/C++ 内存管理的脉络。
一、先搞懂:内存区域到底怎么划分?
在程序运行时,内存会被划分为不同区域,每个区域承担不同职责。理解这些区域,是搞懂变量存储和内存分配的基础。以常见的 32 位系统为例,内存布局从高地址到低地址依次为:
| 内存区域 | 核心作用 | 关键特点 |
|---|---|---|
| 内核空间 | 操作系统内核使用 | 用户代码无法读写,属于系统级区域 |
| 栈区 | 存储局部变量、函数参数 | 自动管理(函数进栈分配、函数返回释放),向下增长 |
| 内存映射段 | 加载动态库、文件映射 | 用于实现进程间通信、动态链接等 |
| 堆区 | 动态内存分配 | 手动管理(需开发者申请 / 释放),向上增长 |
| 静态区(数据段) | 存储全局变量、静态变量 | 程序启动时分配,程序结束后释放 |
| 常量区(代码段) | 存储可执行代码、字符串常量 | 只读属性,尝试修改会导致运行时错误 |
二、实战辨析:变量都存在哪里?
光了解区域划分还不够,关键要能判断具体变量的存储位置。我们结合一段经典代码,逐个分析变量的存储区域(选项:A. 栈 B. 堆 C. 数据段 (静态区) D. 代码段 (常量区))。
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); // 动态内存指针
free(ptr1);
}
关键变量存储位置解析
- globalVar(全局变量):C 选项(数据段)全局变量不属于任何函数,程序启动时在数据段分配空间,程序结束后释放。
- staticGlobalVar(静态全局变量):C 选项(数据段)静态变量(无论全局还是局部)均存储在数据段,区别仅在于作用域(静态全局变量作用域为整个文件)。
- staticVar(静态局部变量):C 选项(数据段)虽然定义在函数内,但
static修饰使其脱离栈区,存储在数据段,生命周期与程序一致。 - localVar(局部变量):A 选项(栈)普通局部变量随函数调用进栈,函数返回时栈帧销毁,变量自动释放。
- num1(局部数组):A 选项(栈)数组是 “内存块实体”,局部数组的整个内存块都在栈区分配。
- char2(字符数组):A 选项(栈)
char2[] = "abcd"是在栈区分配数组空间,并将 “abcd” 从常量区拷贝到栈区数组中。 - *char2(数组内容):A 选项(栈)
char2是数组名,解引用后指向栈区数组的内容。 - pChar3(指针变量):A 选项(栈)指针变量本身是局部变量,存储在栈区,其值是 “abcd” 的地址。
- *pChar3(指针指向内容):D 选项(代码段)“abcd” 是字符串常量,存储在只读的常量区,指针
pChar3仅指向这个常量的地址。 - ptr1(动态内存指针):A 选项(栈)指针变量
ptr1在栈区,存储的是malloc分配的堆内存地址。 - *ptr1(动态内存内容):B 选项(堆)
malloc从堆区申请空间,解引用ptr1访问的是堆区的内存。
三、易混点:数组名 vs 指针,到底有啥区别?
很多人会把数组名和指针画等号,但实际上二者本质不同。我们通过代码和sizeof验证,彻底分清二者差异:
1. 本质区别
| 类型 | 本质 | 存储位置 | 核心特点 |
|---|---|---|---|
数组名(如int num1[10]) |
内存块标识符 | 栈区(局部数组)/ 数据段(全局数组) | 代表整个数组,sizeof(num1)返回数组总字节数(如 10 个 int 为 40 字节) |
指针变量(如int* ptr1) |
存储地址的变量 | 栈区 | 独立变量,仅存储地址,sizeof(ptr1)返回指针大小(32 位系统 4 字节,64 位系统 8 字节) |
2. 代码验证
int num1[10];
int* ptr1 = (int*)malloc(sizeof(int)*10);
printf("数组大小:%zu\n", sizeof(num1)); // 输出40(10*4)
printf("指针大小:%zu\n", sizeof(ptr1)); // 输出4(32位)或8(64位)
3. 关键结论
- 数组名在表达式中会退化为首元素地址(如
num1 == &num1[0]),但它本身不是指针。 - 堆内存的访问必须通过指针:堆内存没有绑定 “标识符”,指针是连接开发者与堆内存的唯一桥梁,丢失指针会导致堆内存成为 “幽灵内存”(内存泄漏)。
四、进阶:const 与指针的三种组合
const修饰指针时,容易混淆 “指针本身不可变” 和 “指针指向内容不可变”。记住一个原则:每加一个const,就限制一个 “可变项”,共三种组合:
1. 指向常量的指针(Pointer to Constant)
- 语法:
const T* ptr或T const* ptr(二者等价) - 特点:指针指向的内容不可变,但指针本身可以指向其他地址。
- 代码示例:
const int* p; int a=10, b=20; p = &a; // ✅ 合法:指针可重定向 *p = 30; // ❌ 非法:不能修改指向的值
2. 常量指针(Constant Pointer)
- 语法:
T* const ptr - 特点:指针本身的地址不可变,但可修改指向的值。
- 代码示例:
int* const p = &a; // 指针指向固定 *p = 30; // ✅ 合法:可修改指向的值 p = &b; // ❌ 非法:不能修改指针地址
3. 指向常量的常量指针
- 语法:
const T* const ptr - 特点:指针地址和指向的值均不可变。
- 代码示例:
const int* const p = &a; *p = 30; // ❌ 非法:值不可变 p = &b; // ❌ 非法:指针地址不可变
五、字符串两种创建方式:常量 vs 数组
字符串的存储是内存管理的高频考点,两种创建方式的差异直接影响 “是否可修改” 和 “存储区域”:
| 特性维度 | 字符串常量(如char* p = "abc") |
字符数组(如char arr[] = "abc") |
|---|---|---|
| 存储区域 | 常量区(只读) | 栈区(局部)/ 数据段(全局 / 静态) |
| 内容可修改? | ❌ 不可修改(修改会导致运行时错误) | ✅ 可修改(如arr[0] = 'x') |
| 生命周期 | 整个程序运行期 | 取决于作用域(函数结束 / 程序结束) |
| 常见用途 | 存储不变的提示信息、配置字 | 存储需处理、拼接的字符串数据 |
六、重点:strlen与sizeof的区别
strlen和sizeof都能计算 “长度”,但适用场景完全不同,误用会导致严重 bug:
1. 核心差异
sizeof:运算符,计算变量 / 类型占用的内存字节数,编译时确定结果。strlen:函数,计算字符串的有效长度(从起始地址到\0前的字符数),运行时确定结果。
2. 代码示例(基于前文 Test 函数)
int num1[10] = {1,2,3,4}; // 数组
char char2[] = "abcd"; // 字符数组
char* pChar3 = "abcd"; // 字符串常量指针
int* ptr1 = (int*)malloc(sizeof(int)*4); // 堆指针
sizeof(num1) = 40; // 10个int,10*4=40字节
sizeof(char2) = 5; // 包含'\0',"abcd"+'\0'共5字节
strlen(char2) = 4; // 不包含'\0',有效字符4个
sizeof(pChar3) = 4; // 指针大小(32位系统)
strlen(pChar3) = 4; // 字符串有效长度
sizeof(ptr1) = 4; // 指针大小(32位系统)
七、底层原理:new/delete与malloc/free的区别
malloc/free是 C 标准库函数,new/delete是 C++ 操作符,二者都是从堆区申请内存,但new/delete是malloc/free的 “升级版”,解决了很多痛点。
1. 核心区别对比
| 对比维度 | malloc/free |
new/delete |
|---|---|---|
| 本质 | 函数 | 操作符 |
| 初始化 | 不支持(申请的空间是 “脏内存”) | 支持(内置类型可直接初始化,自定义类型调用构造函数) |
| 空间大小计算 | 需手动计算(如malloc(sizeof(int)*10)) |
自动计算(如new int[10],无需写sizeof) |
| 返回值类型 | void*,需强转 |
对应类型指针,无需强转 |
| 失败处理 | 返回NULL,需手动判空 |
抛bad_alloc异常,需捕获处理 |
| 自定义类型支持 | 仅开辟 / 释放空间,不调用构造 / 析构函数 | 开辟空间后调用构造函数,释放前调用析构函数 |
2. new/delete的底层实现
new和delete本质是通过operator new和operator delete函数实现的:
operator new:内部调用malloc申请空间,申请失败时尝试执行用户自定义的 “空间不足应对措施”,若没有则抛异常。operator delete:内部调用free释放空间,释放前会先调用对象的析构函数(自定义类型)。
自定义类型的new/delete流程
-
new A(单个对象):- 调用
operator new(sizeof(A))申请空间; - 在申请的空间上调用
A的构造函数,初始化对象。
- 调用
-
delete p(单个对象):- 调用
p指向对象的析构函数,清理资源; - 调用
operator delete(p)释放空间。
- 调用
-
new A[5](对象数组):- 调用
operator new[](内部调用operator new)申请空间,额外多开辟 4 字节存储对象个数; - 调用 5 次
A的构造函数,初始化每个对象。
- 调用
-
delete[] p(对象数组):- 根据多开辟的 4 字节确定对象个数,调用 5 次析构函数;
- 调用
operator delete[](内部调用operator delete)释放空间。
八、避坑指南:这些错误千万别犯!
内存管理的坑很多,尤其是new[]与delete的搭配错误,会导致内存泄漏或程序崩溃。
1. 错误 1:用delete释放new[]的空间
A* p = new A[10]; // new[]申请对象数组
delete p; // ❌ 错误:应使用delete[]
- 后果:
delete不会读取 “多开辟的 4 字节”,仅调用 1 次析构函数,导致 9 个对象的资源未清理(内存泄漏),且释放空间时少释放 4 字节。
2. 错误 2:用realloc修改new申请的空间
new开辟的空间不支持realloc,realloc是 C 标准库函数,仅与malloc搭配使用。
3. 错误 3:频繁申请 / 释放堆内存
频繁的堆内存分配会导致内存碎片(堆区被分割成多个小的空闲块,无法满足大内存申请),而栈区由于自动管理,不存在此问题。
九、高级用法:定位 new(Placement new)
定位 new 是 C++ 的特殊语法,用于在已分配的原始内存上调用构造函数初始化对象,主要配合内存池使用(内存池分配的内存未初始化,需手动调用构造函数)。
1. 语法格式
new (place_address) type; // 无参构造
new (place_address) type(init_val); // 带参构造
place_address:已分配的原始内存地址(指针)。
2. 代码示例(内存池场景)
int main() {
// 步骤1:从内存池分配原始内存(仅分配,不构造)
A* p1 = (A*)operator new(sizeof(A));
// 步骤2:定位new调用构造函数,初始化对象
new(p1) A(10); // 在p1指向的内存上构造A对象
// 步骤3:使用对象...
// p1->doSomething();
// 步骤4:显式调用析构函数,清理资源(内存池不释放内存)
p1->~A();
// 步骤5:释放原始内存(返还给内存池)
operator delete(p1);
return 0;
}
十、总结
C/C++ 内存管理看似复杂,但只要理清 “内存区域→变量存储→动态分配→底层原理” 的逻辑,就能逐步掌握:
- 变量存储位置:全局 / 静态变量在数据段,局部变量 / 数组在栈区,动态内存在堆区,字符串常量在常量区。
- 数组名≠指针:数组名是内存块标识符,指针是存储地址的变量,
sizeof结果差异是关键区分点。 new/delete是malloc/free的升级版:支持初始化、自动调用构造 / 析构函数,失败抛异常。- 避坑核心:
new[]必须搭配delete[],定位 new 用于内存池,避免频繁分配堆内存。


更多推荐


所有评论(0)