欢迎光临
我们一直在努力

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

前言

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 析构函数,再 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:

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

  • 典型场景:内存池、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创建对象,调用成员函数会崩溃?

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

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

  • 赞(0)
    未经允许不得转载:171主机测评 » C/C++ 内存分区、malloc/free 与 new/delete、模板深度详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址