C/C++ 内存分区、malloc/free 与 new/delete、模板深度详解

前言

Hello 大家好,我是TIM

学习 C/C++ 最核心的难点就是内存管理面向对象内存模型,本文结合课堂手绘原理图,一次性梳理四大核心知识点:

  1. 进程虚拟地址空间四区划分(栈 / 堆 / 静态数据区 / 代码常量区)

  2. 各类变量、字符串、动态内存的存储位置辨析

  3. malloc/freenew/delete 底层原理、完整区别、数组释放坑点

  4. 定位 new、内存池池化技术、函数模板推演实例

一、进程虚拟地址空间(32 位 / 64 位)

1. 地址空间大小

  • 32 位程序:寻址范围 $$2^{32}=4G$$,整个进程独享 4G 虚拟地址空间;

  • 64 位程序:寻址范围 $$2^{64$$,远大于物理内存,系统通过虚拟内存分页映射。

2. 四大内存分区(从上至下)

  1. 栈(Stack)

    1. 存储:局部变量、函数形参、函数栈帧(ebp/esp 寄存器维护)、临时对象、数组局部变量;

    2. 特性:自动开辟自动释放,函数执行完毕栈帧直接销毁,无需手动管理;空间大小受限(Windows 默认 1M 左右),不适合大数组。

  2. 堆(Heap)

    1. 存储:malloc/calloc/realloc/freenew/delete 动态申请的内存;

    2. 特性:手动申请手动释放,生命周期不受函数限制,大小几乎无上限;堆空间由操作系统统一管理,频繁分配释放会产生内存碎片。

  3. 静态数据区(数据段 .data/.bss)

    1. 存储:全局变量、static 修饰静态变量(全局静态 / 函数内静态);

    2. .data:初始化过的静态 / 全局变量;

    3. .bss:未初始化全局 / 静态变量,程序运行前统一置 0。

  4. 代码 / 常量区(文本段 .text/.rodata)

    1. .text:编译后的机器指令、函数代码;

    2. .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) 完整两步
  1. operator new:底层调用malloc,在堆上开辟一块对应类大小的原始内存,仅分配不初始化

  2. 调用类的构造函数,执行成员初始化列表、构造函数体,给对象成员赋值; 汇编层面:先 call operator new 分配内存,再 call 类构造函数,填充_a/_top/_capacity成员。

(2)delete p1 完整两步
  1. 调用类析构函数:释放类内部堆资源(比如栈里_a数组),清理对象内部资源;

  2. operator delete:底层调用free,释放对象本身占用的堆内存; 汇编层面:先 call 析构函数,再 call operator 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++ 专属,编译器内置支持

初始化能力

仅分配原始内存,不会调用构造函数,内存随机脏值

分配内存后自动调用构造函数,支持自定义初始化传参

类型安全

返回void*,必须强制类型转换,无类型校验

返回对应类型指针,无需强转,编译期类型检查

内存计算

手动计算sizeof(类型)*数量,容易算错

自动推导类型大小,编译器计算内存

异常处理

分配失败返回NULL,需要手动判空

分配失败默认抛bad_alloc异常,可用try-catch捕获

自定义类资源

仅开辟空间,不执行析构,内部堆内存泄漏

释放前自动调用析构,清理类内动态资源

3. 补充:定位 new(placement-new)

原理图中new (p) T(value) 即为定位 new:

  1. 不分配堆内存,仅在已分配好的内存地址上调用构造函数,完成对象构造;

  2. 典型场景:内存池、STL 容器底层分配器;

  3. 配套销毁:不能直接delete,手动调用ptr->~T() 执行析构。

三、内存池池化技术(高频内存优化方案)

1. 为什么需要内存池?

频繁调用malloc/freenew/delete会出现两个严重问题:

  1. 系统调用开销大,分配速度慢;

  2. 大量小块内存频繁创建销毁,产生内存碎片,堆利用率下降。

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&)
}
  1. 编译器看到Swap(int,int):生成 int 版本 Swap 函数;

  2. 看到Swap(double,double):生成 double 版本 Swap 函数;

  3. 每种不同类型都会生成一份独立函数代码,实现一套模板多类型复用。

3. 模板优缺点

  • 优点:代码复用、类型安全、编译期类型校验、无需重复写重载函数;

  • 缺点:多类型实例化会导致代码膨胀(每个类型一份函数),报错信息晦涩难懂。

五、核心知识点总结

  1. 四区记忆:局部栈、动态堆、静态全局 static 区、只读代码常量区;区分字符串数组与字符串指针存储位置是面试高频考点;

  2. new/delete 两步走:new = malloc + 构造函数;delete = 析构函数 + free;数组必须配对new[]/delete[]

  3. malloc 和 new 分水岭:是否调用构造 / 析构函数,是否自动初始化,是否类型安全;

  4. 内存池优化:批量预分配内存,减少系统调用,解决内存碎片;定位 new 是池化技术底层核心;

  5. 模板核心:蓝图不生成代码,调用时编译器自动推演实例化,实现泛型编程。

拓展面试题(可自行练习)

  1. char arr[] = "123"char* p = "123" 存储区别,修改各自内容会发生什么?

  2. 为什么自定义类只用malloc创建对象,调用成员函数会崩溃?

  3. 不使用内存池,频繁创建销毁链表节点会有什么性能问题?

  4. 模板支持哪些推演方式?显示指定模板参数怎么写?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值