C++ 内存管理全面复习:从内存分布到 operator new/delete

本文是《5.C/C++内存管理》的复习整理,按"内存区域划分 → C 的动态内存方式 → C++ 的 new/delete → 底层原理 → 高级用法 → 对比总结"的顺序梳理,代码统一放在 namespace Jianyi 下方便测试。文末附高频面试问法,方便查漏补缺。

一、C/C++ 内存分布

1.1 一段代码引出的问题

先看一段很经典的"考察内存分布"的代码:

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(数据段) 全局变量,编译期就分配好地址,属于静态区
staticGlobalVar C(数据段) 加了 static,本质还是全局数据,只是链接属性变了(外部→内部),存储位置不变
staticVar C(数据段) 函数内的静态局部变量,生命周期跟全局变量一样,存在数据段
localVar A(栈) 非静态局部变量,函数调用时在栈上分配
num1 A(栈) 数组本身是局部变量,整个数组都开在栈上
char2 A(栈) char2 是数组变量本身,在栈上;但它的内容 "abcd" 是被拷贝进这块栈空间的
*char2 A(栈) 解引用取的是数组第一个字符,这块内存在栈上
pChar3 A(栈) pChar3 是一个指针变量,指针变量本身是局部变量,存在栈上
*pChar3 D(代码段/常量区) pChar3 指向 的字符串常量 "abcd" 存放在常量区,pChar3 只是存了这块常量区的地址
ptr1 A(栈) ptr1 这个指针变量本身在栈上
*ptr1 B(堆) ptr1 指向的是 malloc 申请出来的空间,在堆上

易错对比点(面试常问)char2pChar3 看起来都存了 "abcd",但本质完全不同------char2 是一个数组"abcd" 是被拷贝进栈上这块数组空间的,内容可以修改pChar3 是一个指针 ,它指向的是位于常量区的字符串常量,*pChar3 试图修改会导致未定义行为(很多平台上直接崩溃)。这是一个非常经典的考点。

1.2 内存区域划分图

C/C++ 程序运行时的内存,从高地址到低地址大致可以划分为(内核空间用户不可读写,不算在内):

bash 复制代码
高地址
┌─────────────────────────┐
│   内核空间(用户不可读写)│
├─────────────────────────┤
│   栈(向下增长 ↓)       │   非静态局部变量 / 函数参数 / 返回值
├─────────────────────────┤
│                         │
├─────────────────────────┤
│   内存映射段             │   文件映射、动态库、匿名映射
├─────────────────────────┤
│                         │
├─────────────────────────┤
│   堆(向上增长 ↑)       │   malloc/new 动态申请的空间
├─────────────────────────┤
│   数据段                 │  全局数据、静态数据
├─────────────────────────┤
│   代码段                 │  可执行代码 / 只读常量
└─────────────────────────┘
低地址

要点梳理

  1. **栈(stack)**又叫堆栈,存放非静态局部变量、函数参数、返回值等,栈是向下增长的(地址越用越小)。
  2. 内存映射段是高效的 I/O 映射方式,用来装载共享的动态库;用户也可以用系统接口创建共享内存实现进程间通信(Linux 课程会详细展开,这里先了解概念)。
  3. **堆(heap)**用于程序运行时动态内存分配,堆是向上增长的(地址越用越大)------注意和栈的增长方向刚好相反,这也是经典面试题"栈和堆的区别"里常考的一点。
  4. 数据段 :存储全局数据和静态数据(包括 static 修饰的全局/局部变量)。
  5. 代码段 :存放可执行的代码,以及只读常量(比如字符串字面量 "abcd")。

二、C 语言中的动态内存管理方式

C 语言提供了 4 个动态内存管理函数:malloccallocreallocfree

cpp 复制代码
namespace Jianyi
{
    void Test()
    {
        int* p2 = (int*)calloc(4, sizeof(int));
        int* p3 = (int*)realloc(p2, sizeof(int) * 10);

        // 这里需要再手动 free(p2) 吗?
        // 不需要!realloc 内部已经处理好了旧内存:
        // 如果原地扩容成功,p2 和 p3 其实是同一块地址;
        // 如果原地放不下,realloc 会在别处开辟新空间、把旧数据拷过去,
        // 并自动释放掉 p2 原来的那块空间。
        // 所以 p2 这个指针在 realloc 调用之后就已经失效了,
        // 不能再对 p2 单独调用 free,只需要 free(p3) 即可。
        free(p3);
    }
}

【面试题 1】malloc / calloc / realloc 的区别?

函数 作用
malloc(size) 申请一块 size 字节的空间,不做初始化,里面是随机的脏数据
calloc(n, size) 申请 n × size 字节的空间,并且把这块空间清零(初始化为 0)
realloc(ptr, newSize) 调整 ptr 指向空间的大小到 newSize原地能扩容就原地扩;原地放不下就在别处重新开辟、把旧数据拷贝过去、并释放旧空间,返回新地址

【面试题 2】malloc 的实现原理?

这是一个比较深的问题,简单来说:malloc 底层通常基于操作系统提供的 brk/sbrk(扩展堆顶)或 mmap(大块内存走内存映射)向操作系统申请一大块内存,再通过空闲链表(free list)+ 内存池 的方式对这块内存进行切分管理,尽量减少内存碎片、提高分配效率。感兴趣可以深入读一下 glibc 里 ptmalloc 的实现(glibc中malloc实现原理)。这个问题面试时不一定要求手写实现,但要能说出"分级管理、空闲链表、系统调用申请大块内存"这几个关键词。

三、C++ 内存管理方式:new 和 delete

C 语言那一套内存管理方式在 C++ 里依然能用,但有些场景就无能为力了(比如自定义类型对象 申请空间时,光开辟内存是不够的,还需要初始化 )。因此 C++ 又提出了自己的方式:通过 newdelete 操作符进行动态内存管理

3.1 new/delete 操作内置类型

cpp 复制代码
namespace Jianyi
{
    void Test()
    {
        // 动态申请一个 int 类型的空间
        int* ptr4 = new int;

        // 动态申请一个 int 类型的空间并初始化为 10
        int* ptr5 = new int(10);

        // 动态申请 3 个 int 类型的空间(连续空间)
        int* ptr6 = new int[3];

        delete ptr4;
        delete ptr5;
        delete[] ptr6;   // 注意:数组要配套用 delete[]
    }
}

注意(重点) :申请和释放单个元素 的空间,用 newdelete;申请和释放连续空间 (数组),用 new[]delete[]两者必须匹配起来使用new[]delete(或反过来)是未定义行为,很多编译器不会报错但实际上是隐患,面试中经常被追问"如果不匹配会发生什么"。

3.2 new/delete 操作自定义类型

cpp 复制代码
namespace Jianyi
{
    class A
    {
    public:
        A(int a = 0) : _a(a)
        {
            cout << "A():" << this << endl;
        }

        ~A()
        {
            cout << "~A():" << this << endl;
        }

    private:
        int _a;
    };

    void Test()
    {
        // new/delete 和 malloc/free 最大的区别:
        // new/delete 对于【自定义类型】除了开空间,还会调用构造函数和析构函数
        A* p1 = (A*)malloc(sizeof(A));   // 只开空间,不会调用构造函数
        A* p2 = new A(1);                // 开空间 + 调用构造函数
        free(p1);                        // 只释放空间,不会调用析构函数
        delete p2;                       // 调用析构函数 + 释放空间

        // 内置类型上,new/malloc 几乎是一样的
        int* p3 = (int*)malloc(sizeof(int));  // C 方式
        int* p4 = new int;                    // C++ 方式
        free(p3);
        delete p4;

        // 数组同理,要配套用 new[]/delete[]
        A* p5 = (A*)malloc(sizeof(A) * 10);
        A* p6 = new A[10];
        free(p5);
        delete[] p6;
    }
}

注意(核心结论) :在申请自定义类型 的空间时,new 会调用构造函数,delete 会调用析构函数,而 mallocfree 不会 。这是 new/delete 相比 malloc/free 最本质的进步------它把"开空间"和"初始化对象/清理资源"这两件事整合到了一起。

四、operator new 与 operator delete 函数(重点)

4.1 关系梳理

这里有一组非常容易被搞混的名字,一定要分清楚:

  • new / delete:是用户使用的操作符,程序员平时写代码用的就是它们。
  • operator new / operator delete:是系统提供的全局函数new 操作符在底层会调用 operator new 这个全局函数来申请空间,delete 操作符在底层会调用 operator delete 这个全局函数来释放空间。

也就是说,new 不是直接去问操作系统要内存的,而是"包了一层":new 内部调 operator new 申请空间 → 再在这块空间上执行构造函数。

4.2 operator new 的实现(简化版源码解读)

cpp 复制代码
/*
operator new:该函数实际通过 malloc 来申请空间。
如果 malloc 申请空间成功,直接返回;
如果申请失败,尝试执行"空间不足应对措施",
如果用户设置了该应对措施,就继续重试申请,否则抛异常。
*/
void* operator new(size_t size)
{
    void* p;
    while ((p = malloc(size)) == 0)
    {
        if (/* 没有设置应对措施 */ true)
        {
            // 申请内存失败,抛出 bad_alloc 类型异常
            static const std::bad_alloc nomem;
            throw nomem;
        }
        // 否则继续循环重试
    }
    return p;
}

4.3 operator delete 的实现(简化版源码解读)

cpp 复制代码
/*
operator delete:该函数最终是通过 free 来释放空间的
*/
void operator delete(void* pUserData)
{
    if (pUserData == NULL)
        return;
    // ......内部最终会调用 free(pUserData) 完成释放
}

小结 :通过看这两个函数的(简化)实现可以确认:operator new 实际也是通过 malloc 来申请空间的 ------如果 malloc 成功就直接返回;如果失败,就执行用户提供的"空间不足应对措施",用户提供了就继续申请,没提供就抛异常。operator delete 最终是通过 free 来释放空间的 。所以从最底层看,new/delete 依然是建立在 malloc/free 之上的一层封装,只是多加了"构造/析构"和"异常机制"。

五、new 和 delete 的实现原理

5.1 内置类型

如果申请的是内置类型 的空间,new/mallocdelete/free 基本类似,不同的地方是:

  • new/delete 申请和释放的是单个元素 的空间,new[]/delete[] 申请的是连续空间
  • new 在申请空间失败时会抛异常 ,而 malloc返回 NULL (这也是为什么用 malloc 必须判空,而 new 一般不需要判空,但需要考虑捕获异常)。

5.2 自定义类型

这一部分是本章的核心,一定要背熟四个原理:

new 的原理

  1. 调用 operator new 函数申请空间。
  2. 在申请的空间上执行构造函数,完成对象的构造。

delete 的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作。
  2. 调用 operator delete 函数释放对象的空间。

new T[N] 的原理

  1. 调用 operator new[] 函数,而 operator new[] 内部实际上是调用 operator new 函数完成 N 个对象空间的申请。
  2. 在申请到的空间上执行 N 次构造函数。

delete[] 的原理

  1. 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理。
  2. 调用 operator delete[] 释放空间,实际上是在 operator delete[] 内部调用 operator delete 来完成真正的空间释放。

可以看出,new[]/delete[] 本质上是在 new/delete 的基础上,加了一层"批量处理 N 个对象"的逻辑,底层最终还是落到同一套 operator new/operator delete 上。这也解释了为什么 new[] 一定要配 delete[]------只有 delete[] 才知道要执行 N 次 析构函数,普通的 delete 只会析构一个对象,后面 N-1 个对象的资源就泄漏了。

六、定位 new 表达式(placement-new)(了解)

6.1 概念

定位 new 表达式 是在已经分配好的原始内存空间中,调用构造函数初始化一个对象。

使用格式

复制代码
new (place_address) type
// 或者
new (place_address) type(initializer-list)

其中 place_address 必须是一个指针,initializer-list 是类型的初始化列表(构造函数参数)。

6.2 使用场景

定位 new 表达式在实际工程中一般是配合内存池使用 的。因为内存池分配出来的内存是"裸内存",没有经过初始化,如果要在这块裸内存上构造一个自定义类型的对象,就必须使用定位 new 表达式来显式调用构造函数进行初始化------这正是内存池、对象池这类底层组件的核心技术之一。

cpp 复制代码
namespace Jianyi
{
    class A
    {
    public:
        A(int a = 0) : _a(a)
        {
            cout << "A():" << this << endl;
        }

        ~A()
        {
            cout << "~A():" << this << endl;
        }

    private:
        int _a;
    };

    int main()
    {
        // p1 现在指向的只是一段与 A 对象大小相同的裸内存,
        // 还不能算是一个真正的 A 对象,因为构造函数没有执行
        A* p1 = (A*)malloc(sizeof(A));
        new(p1) A;          // 定位 new:在 p1 这块空间上调用构造函数
                              // 注意:如果 A 的构造函数有参数,这里需要传参
        p1->~A();            // 用完要显式调用析构函数(因为不是用 delete 释放的)
        free(p1);             // 最后用 free 释放这块裸内存

        // 也可以配合 operator new 一起使用
        A* p2 = (A*)operator new(sizeof(A));
        new(p2) A(10);
        p2->~A();
        operator delete(p2);

        return 0;
    }
}

这段代码体现的核心思路是malloc/operator new 只负责"要一块裸内存",定位 new 只负责"在这块内存上把对象构造出来",两者是解耦的------这正是内存池能够复用内存块、又能正确构造不同对象的关键所在。

七、malloc/free 和 new/delete 的区别(高频总结题)

共同点 :都是从堆上 申请空间,并且都需要用户手动释放

不同点

  1. malloc/free函数new/delete操作符
  2. malloc 申请的空间不会初始化new 可以初始化 (如 new int(10))。
  3. malloc 申请空间时需要手动计算空间大小并传递new 只需在其后跟上空间的类型即可,如果是申请多个对象,用 [] 指定对象个数即可,编译器自动算大小。
  4. malloc 的返回值是 void*,使用时必须强转 成目标类型指针;new 不需要,因为 new 后面跟的就是空间的类型,编译器直接返回对应类型的指针。
  5. malloc 申请空间失败时返回 NULL,因此使用时必须判空new 申请失败时不需要判空,但需要考虑捕获异常new 失败会抛 bad_alloc)。
  6. 申请自定义类型 对象时,malloc/free 只会开辟/释放空间,不会 调用构造函数与析构函数;而 new 在申请空间后会调用构造函数 完成对象的初始化,delete 在释放空间前会调用析构函数完成资源的清理释放------这是两者最核心、最常被考到的区别。

八、复习自测 / 面试高频问法

  1. C/C++ 程序运行时的内存从上到下大致分为哪几个区域?各自存什么?
  2. 栈和堆的增长方向分别是怎样的?
  3. 局部字符数组 char2[] 和字符指针 const char* pChar3 都存了同样的字符串内容,两者的存储位置有什么本质区别?为什么 *pChar3 不能被修改?
  4. malloccallocrealloc 三者分别做了什么?realloc 扩容失败/成功时,原来的指针还有效吗?
  5. new/deletemalloc/free 最本质的区别是什么?(提示:构造/析构)
  6. operator newnew 是什么关系?operator new 底层是靠什么申请内存的?
  7. new T[N]delete[] 的原理分别是什么?为什么 new[] 必须配 delete[],不能用普通的 delete
  8. 什么是定位 new 表达式(placement-new)?它一般用在什么场景?
  9. 用定位 new 构造出来的对象,应该怎么正确地销毁和释放内存?(提示:不能直接 delete
  10. 总结一下 malloc/freenew/delete 的所有区别。

相关推荐
xcLeigh1 小时前
Go入门:main包与main函数的特殊地位
开发语言·后端·golang
玖玥拾1 小时前
LeetCode 58 最后一个单词的长度
算法·leetcode
SilentSlot1 小时前
【C/C++】手写 DPDK 协议栈(八):用五元组 Hash 加速连接定位
c语言·c++·哈希算法
LONGZETECH2 小时前
工业实训仿真设计实践:电机拆装软件的 DAG 流程建模、工具精度分级与数据体系搭建
大数据·算法·unity·架构·汽车
hanlin032 小时前
刷题笔记:力扣第19题-删除链表的倒数第N个结点
笔记·leetcode·链表
我是人✓2 小时前
SQL主键与外键
java·数据库
心念枕惊2 小时前
PHP 在领域驱动(DDD)设计中的核心实践
android·开发语言·php
圣光SG2 小时前
Java操作题练习(二)
java·开发语言·python
m0_617493942 小时前
Python OpenCV 分水岭算法(Watershed)详解与实战
python·opencv·算法