前言
Hello 大家好,我是TIM
学习 C/C++ 最核心的难点就是内存管理与面向对象内存模型,本文结合课堂手绘原理图,一次性梳理四大核心知识点:
-
进程虚拟地址空间四区划分(栈 / 堆 / 静态数据区 / 代码常量区)
-
各类变量、字符串、动态内存的存储位置辨析
-
malloc/free和new/delete底层原理、完整区别、数组释放坑点 -
定位 new、内存池池化技术、函数模板推演实例
一、进程虚拟地址空间(32 位 / 64 位)
1. 地址空间大小
-
32 位程序:寻址范围 $$2^{32}=4G$$,整个进程独享 4G 虚拟地址空间;
-
64 位程序:寻址范围 $$2^{64$$,远大于物理内存,系统通过虚拟内存分页映射。
2. 四大内存分区(从上至下)
-
栈(Stack)
-
存储:局部变量、函数形参、函数栈帧(
ebp/esp寄存器维护)、临时对象、数组局部变量; -
特性:自动开辟自动释放,函数执行完毕栈帧直接销毁,无需手动管理;空间大小受限(Windows 默认 1M 左右),不适合大数组。
-
-
堆(Heap)
-
存储:
malloc/calloc/realloc/free、new/delete动态申请的内存; -
特性:手动申请手动释放,生命周期不受函数限制,大小几乎无上限;堆空间由操作系统统一管理,频繁分配释放会产生内存碎片。
-
-
静态数据区(数据段 .data/.bss)
-
存储:全局变量、
static修饰静态变量(全局静态 / 函数内静态); -
.data:初始化过的静态 / 全局变量; -
.bss:未初始化全局 / 静态变量,程序运行前统一置 0。
-
-
代码 / 常量区(文本段 .text/.rodata)
-
.text:编译后的机器指令、函数代码; -
.rodata:只读常量,字符串字面量"abcd"、const char*指向的常量字符串,不可修改。
-
3. 变量存储位置选择题解析
int globalVar = 1; // C 静态数据区
static int staticGlobalVar = 1; // C 静态数据区
void Test()
{
static int staticVar = 1; // C 静态数据区(函数静态,只初始化一次)
int localVar = 1; // A 栈(局部变量)
int num1[10] = {1,2,3,4}; // A 栈(局部数组,数组本体在栈)
char char2[] = "abcd"; // A 栈:char2数组在栈,运行时把常量区"abcd"拷贝到栈数组,可修改
const char* pChar3 = "abcd"; // D 常量区:指针pChar3存在栈,字符串字面量"abcd"存在只读常量区,不可修改
int* ptr1 = (int*)malloc(sizeof(int)*4); // A栈存指针变量ptr1,B堆存malloc申请的4个int空间
}
free(ptr1);
总结区分口诀:
-
[]字符数组:字符串拷贝到栈,可读可写; -
const char*字符串指针:指针在栈,字面量在常量区,只读; -
static/全局:全部进静态区; -
malloc/new出来的数据本体在堆,保存地址的指针变量在栈。
二、malloc/free VS new/delete 底层原理与完整区别
1. 底层执行流程(结合汇编原理图)
(1)new Stack(10) 完整两步
-
operator new:底层调用malloc,在堆上开辟一块对应类大小的原始内存,仅分配不初始化; -
调用类的构造函数,执行成员初始化列表、构造函数体,给对象成员赋值; 汇编层面:先 call
operator new分配内存,再 call 类构造函数,填充_a/_top/_capacity成员。
(2)delete p1 完整两步
-
调用类析构函数:释放类内部堆资源(比如栈里
_a数组),清理对象内部资源; -
operator delete:底层调用free,释放对象本身占用的堆内存; 汇编层面:先 call 析构函数,再 calloperator delete归还堆内存。
(3)数组 new [] /delete [] 特殊坑点
Stack* p = new Stack[10]; delete[] p; // 必须匹配[]
-
new[]:堆头部会额外存储对象个数(原理图中红色数字 10); -
delete[]:读取头部计数,循环调用 N 次析构函数,再释放整块内存; -
错误写法
delete p;:只会调用 1 次析构,剩下 9 个对象资源未释放,造成严重内存泄漏。
2. 五大核心区别
|
对比维度 |
malloc/free |
new/delete |
|
语法属性 |
库函数,C 语言原生 |
运算符,C++ 专属,编译器内置支持 |
|
初始化能力 |
仅分配原始内存,不会调用构造函数,内存随机脏值 |
分配内存后自动调用构造函数,支持自定义初始化传参 |
|
类型安全 |
返回 |
返回对应类型指针,无需强转,编译期类型检查 |
|
内存计算 |
手动计算 |
自动推导类型大小,编译器计算内存 |
|
异常处理 |
分配失败返回 |
分配失败默认抛 |
|
自定义类资源 |
仅开辟空间,不执行析构,内部堆内存泄漏 |
释放前自动调用析构,清理类内动态资源 |
3. 补充:定位 new(placement-new)
原理图中new (p) T(value) 即为定位 new:
-
不分配堆内存,仅在已分配好的内存地址上调用构造函数,完成对象构造;
-
典型场景:内存池、STL 容器底层分配器;
-
配套销毁:不能直接
delete,手动调用ptr->~T()执行析构。
三、内存池池化技术(高频内存优化方案)
1. 为什么需要内存池?
频繁调用malloc/free、new/delete会出现两个严重问题:
-
系统调用开销大,分配速度慢;
-
大量小块内存频繁创建销毁,产生内存碎片,堆利用率下降。
2. 内存池核心思路
提前一次性向堆申请一大块连续内存作为内存池,程序需要小块内存时直接从池中取,释放时归还池内,不直接调用系统free;程序退出时统一释放整块池内存。
-
适用场景:链表节点、游戏实体、高频创建销毁的短生命周期对象;
-
延伸技术:线程池、连接池,思想完全一致 —— 提前批量申请资源,复用减少系统交互。
3. STL 中的应用
STL 容器list/map底层默认使用list_node_allocator内存分配器:
-
allocate():从内存池取出空闲节点; -
deallocate():节点归还内存池,不释放给操作系统; -
construct/destroy:定位 new 手动构造、析构节点对象。
四、函数模板:编译器自动推演实例化
1. 模板本质
模板是代码蓝图,本身不生成可执行代码;编译器根据你传入的实参类型,自动推演、生成对应类型的重载函数,这个过程叫模板实例化。
2. 推演流程图解
template<class T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
int main()
{
int a=1,b=2;
Swap(a,b); // 推演 T=int,实例化 void Swap(int&,int&)
double c=1.1,d=2.2;
Swap(c,d); // 推演 T=double,实例化 void Swap(double&,double&)
char ch1='a',ch2='b';
Swap(ch1,ch2);// 推演 T=char,实例化 void Swap(char&,char&)
}
-
编译器看到
Swap(int,int):生成 int 版本 Swap 函数; -
看到
Swap(double,double):生成 double 版本 Swap 函数; -
每种不同类型都会生成一份独立函数代码,实现一套模板多类型复用。
3. 模板优缺点
-
优点:代码复用、类型安全、编译期类型校验、无需重复写重载函数;
-
缺点:多类型实例化会导致代码膨胀(每个类型一份函数),报错信息晦涩难懂。
五、核心知识点总结
-
四区记忆:局部栈、动态堆、静态全局 static 区、只读代码常量区;区分字符串数组与字符串指针存储位置是面试高频考点;
-
new/delete 两步走:new = malloc + 构造函数;delete = 析构函数 + free;数组必须配对
new[]/delete[]; -
malloc 和 new 分水岭:是否调用构造 / 析构函数,是否自动初始化,是否类型安全;
-
内存池优化:批量预分配内存,减少系统调用,解决内存碎片;定位 new 是池化技术底层核心;
-
模板核心:蓝图不生成代码,调用时编译器自动推演实例化,实现泛型编程。
拓展面试题(可自行练习)
-
char arr[] = "123"和char* p = "123"存储区别,修改各自内容会发生什么? -
为什么自定义类只用
malloc创建对象,调用成员函数会崩溃? -
不使用内存池,频繁创建销毁链表节点会有什么性能问题?
-
模板支持哪些推演方式?显示指定模板参数怎么写?
459

被折叠的 条评论
为什么被折叠?



