尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【c++学习笔记】【基础】8.内存管理——从内存分布到 new/delete 的实现原理

【c++学习笔记】【基础】8.内存管理——从内存分布到 new/delete 的实现原理 前言C 中的内存管理表面上是在学习malloc、free、new、delete等接口但真正需要理解的其实是两个问题第一程序运行时数据存放在哪里动态内存又是如何申请和释放的第二一块内存和一个 C 对象之间到底是什么关系第二个问题尤其重要。对于int这样的内置类型我们很容易把“申请内存”和“创建对象”混在一起。但对于一个拥有构造函数和析构函数的类来说获得一块足够大的内存并不意味着一个对象已经构造完成。这也是理解malloc/free、new/delete、operator new/operator delete以及 placement new 之间关系的关键。本文按照下面这条主线展开内存分布 → C 动态内存管理 → new/delete → 对象生命周期 → new/delete 的实现原理 → placement new一、C/C 程序的内存分布程序运行时不同类型的数据通常存放在不同的内存区域。可以先建立一个简化的内存分布模型高地址 ┌──────────────────────────┐ │ 栈 │ │ 局部变量、函数参数等 │ │ ↓ │ ├──────────────────────────┤ │ 内存映射区域 │ │ 动态库、共享内存等 │ ├──────────────────────────┤ │ ↑ │ │ 堆 │ │ 动态申请的内存 │ ├──────────────────────────┤ │ 数据段 │ │ 全局变量、静态变量 │ ├──────────────────────────┤ │ 代码段 │ │ 程序代码、只读常量 │ └──────────────────────────┘ 低地址常见区域可以简单记成内存区域主要存放内容栈非静态局部变量、函数参数等堆程序运行时动态申请的内存数据段全局变量、静态变量代码段 / 只读区域程序代码、只读常量内存映射区域动态库、共享内存等这里最容易混淆的不是区域本身而是指针变量和它指向的数据可能位于不同的位置。例如int*ptr(int*)malloc(sizeof(int)*4);如果ptr是普通局部变量那么栈 堆 ┌───────────┐ ┌───────────────┐ │ ptr │ ───────────→ │ 动态申请的空间 │ └───────────┘ └───────────────┘ptr本身是一个局部指针变量而malloc得到的动态空间位于堆上。因此分析指针时最好始终分成两个问题指针变量本身在哪里指针指向的数据又在哪里例如charstr[]abcd;constchar*pStrabcd;str是一个字符数组而pStr是一个指针它指向字符串字面量。虽然代码中都出现了abcd但两者的内存关系并不相同。二、C 风格的动态内存管理程序中的普通局部变量会随着作用域自动创建和销毁但有些内存需求只有在程序运行时才能确定。这时就需要动态内存。C 中主要通过下面四个函数管理动态内存malloc calloc realloc free1. mallocmalloc按字节申请一块连续的内存int*p(int*)malloc(sizeof(int)*10);这里需要自己计算需要的空间大小sizeof(int) × 10malloc返回的是void*。在 C 中直接使用时需要显式转换成对应的指针类型。需要注意的是malloc只负责申请空间不负责初始化这块空间中的数据。2. calloccalloc同样用于申请动态内存int*p(int*)calloc(10,sizeof(int));两个参数分别表示元素个数 × 单个元素大小因此上面的代码申请了能够容纳 10 个int的空间。与malloc相比calloc还会将申请得到的字节初始化为 0。3. realloc如果已经有一块动态空间希望调整它的大小可以使用reallocint*p1(int*)calloc(4,sizeof(int));int*p2(int*)realloc(p1,sizeof(int)*10);realloc最值得注意的一点是调整后的地址不保证与原地址相同。如果原空间后面还有足够的连续空间可能直接在原位置扩容如果没有就可能重新寻找一块足够大的空间。因此应该使用realloc返回的新地址继续操作。另外如果realloc成功就不应该再对原来的p1单独执行一次free。成功调整后应使用返回的指针管理这块空间free(p2);实际编程中为了避免realloc失败时直接覆盖原指针更稳妥的写法通常是先用临时指针接收返回值。4. free动态申请的空间使用结束后需要主动释放free(p);同一块已经释放的空间不能再次释放否则会产生未定义行为。到这里C 风格的动态内存管理可以概括成申请空间 ↓ 使用空间 ↓ 必要时调整空间 ↓ 释放空间它解决的核心问题始终是如何管理一块动态申请的原始内存。三、为什么 C 还需要 new/delete既然malloc/free已经能够申请和释放动态内存为什么 C 还要提供new/delete如果只看内置类型两者的区别并不明显int*p1(int*)malloc(sizeof(int));int*p2newint;真正的区别体现在自定义类型上。假设有这样一个类classA{public:A(inta0):_a(a){coutA(): thisendl;}~A(){cout~A(): thisendl;}private:int_a;};分别使用malloc和newA*p1(A*)malloc(sizeof(A));A*p2newA(10);malloc(sizeof(A))做的是申请一块足够容纳 A 的内存。而newA(10);不仅会获得空间还会调用A的构造函数在这块空间上完成对象构造。释放时也是一样free(p1);deletep2;free只负责释放空间而delete会先调用析构函数再释放对象占用的空间。因此对于自定义类型可以先记住最核心的关系malloc 申请空间 free 释放空间 new 申请空间 构造对象 delete 析构对象 释放空间这也是理解 C 内存管理最重要的一点内存和对象不是同一个概念。一块内存只是对象存在的空间基础只有完成了对象的构造对象的生命周期才真正开始。四、new/delete 背后到底发生了什么理解了“内存”和“对象”的区别之后就可以继续拆解new/delete。1. new 与 operator newnew和operator new名字很像但它们并不是同一个东西。例如A*pnewA(10);从原理上可以理解成两个阶段new A(10) │ ┌────────┴────────┐ ▼ ▼ 申请空间 构造对象 │ │ operator new A::A(10)也就是说调用operator new申请对象所需的原始空间在这块空间上调用构造函数完成对象构造。所以new ≠ operator new更准确的理解是new operator new 构造函数operator new负责的是内存分配而new表达式负责的是完整的对象创建过程。在常见实现中operator new会进一步借助底层的内存分配函数获得空间如果普通new最终无法获得内存则会通过异常机制报告失败。这和malloc失败时返回NULL不同malloc 失败 ↓ 返回 NULL 普通 new 失败 ↓ 抛出 std::bad_alloc2. delete 与 operator deletedelete的过程正好相反。deletep;对于自定义类型可以理解为delete p │ ┌────────┴────────┐ ▼ ▼ 析构对象 释放空间 │ │ A::~A() operator delete也就是调用析构函数完成对象内部资源的清理调用operator delete释放对象占用的空间。因此delete 析构函数 operator delete于是一个动态创建的 C 对象其生命周期可以完整地表示为申请空间 ↓ 构造对象 ↓ 使用对象 ↓ 析构对象 ↓ 释放空间这五个步骤是理解整个 C 动态内存管理最重要的一条主线。五、new[] 和 delete[]除了创建单个对象C 还可以动态创建对象数组A*pnewA[10];它的过程可以理解为申请能够容纳多个 A 对象的空间 ↓ 执行多次构造更具体地说new A[10] ↓ operator new[] ↓ 申请数组所需空间 ↓ 执行 10 次构造对应delete[]p;则需要执行 10 次析构 ↓ operator delete[] ↓ 释放数组空间因此new ↔ delete new[] ↔ delete[]必须匹配使用。这并不只是一个需要死记的语法规则。对于自定义类型数组而言new[]/delete[]背后还涉及多个对象的构造与析构本质上仍然是对象生命周期管理的问题。六、Placement New在已有内存上构造对象前面已经把new拆成了申请空间 构造对象那么自然会有一个问题如果我已经有一块内存只想在这块内存上构造对象可以吗可以这就是placement new定位 new。基本形式是new(address)Type;例如A*p(A*)malloc(sizeof(A));此时得到了一块足够容纳A的原始空间但A的构造函数还没有执行。接下来new(p)A(10);就可以在p指向的空间上构造一个A对象。完整过程A*p(A*)malloc(sizeof(A));// 在已有空间上构造对象new(p)A(10);// 显式调用析构函数p-~A();// 释放原始空间free(p);也可以使用operator new获得原始空间A*p(A*)operatornew(sizeof(A));new(p)A(10);p-~A();operatordelete(p);整个过程非常直观获得原始空间 ↓ placement new ↓ 构造对象 ↓ 使用对象 ↓ 显式析构 ↓ 释放原始空间placement new 常用于内存池等需要自行管理原始内存的场景。但在学习这一部分时更重要的是通过它理解申请内存和构造对象是两件事析构对象和释放内存同样是两件事。普通的new/delete只是帮我们把这些步骤组合起来了。七、malloc/free 与 new/delete 的区别理解完前面的原理再来看两套方式的区别会简单很多。对比malloc/freenew/delete本质函数操作符空间大小手动计算字节数根据类型确定返回类型malloc返回void*new直接得到对应类型指针初始化malloc不负责初始化new可以完成初始化申请失败返回NULL普通new抛出异常自定义类型不负责构造和析构调用构造函数和析构函数如果以后复习时不想记整张表只需要先抓住最核心的一点malloc/free主要解决原始内存的申请和释放new/delete在管理内存的同时还要负责 C 对象的构造和析构。八、总结一张图串起 C 内存管理最后把前面的内容压缩成一张关系图C 动态内存管理 │ ┌───────────────┴───────────────┐ ▼ ▼ C 风格内存管理 C 对象管理 │ │ malloc / free new / delete │ │ ▼ ┌──────────┴──────────┐ 原始内存 ▼ ▼ new delete │ │ ┌───────┴───────┐ ┌───────┴───────┐ ▼ ▼ ▼ ▼ operator new 构造 析构 operator delete │ │ ▼ ▼ 申请空间 释放空间如果以后重新复习这一部分可以直接回忆下面几组关系malloc → 申请原始空间 free → 释放原始空间 new → operator new 构造对象 delete → 析构对象 operator delete new[] → 申请数组空间 多次构造 delete[] → 多次析构 释放数组空间 placement new → 在已经存在的原始内存上构造对象最后把整篇内容再压缩成一条对象生命周期申请空间 ↓ 构造对象 ↓ 使用对象 ↓ 析构对象 ↓ 释放空间其中申请空间和释放空间解决的是内存管理问题构造和析构解决的是对象生命周期问题。理解了这两个维度再去看malloc、new、operator new、delete、placement new就不再是一堆零散的 API而是一套完整的 C 动态内存管理逻辑。
返回列表