Re:从零开始的 C++ 入門篇(十一):全站最全面的C/C++内存管理的底层剖析与硬核指南

Re:从零开始的 C++ 入門篇(十一):全站最全面的C/C++内存管理的底层剖析与硬核指南

◆ 博主名称: 晓此方-ZEEKLOG博客

大家好,欢迎来到晓此方的博客。

⭐️C++系列个人专栏:

Re:从零开始的C++_晓此方的博客-ZEEKLOG博客

 ⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰


目录

0.1概要&序論

一,布局模型与常见误区解析

1.1C/C++内存布局

1.2内存布局易误解点

二,复习C语言的内存管理方法

2.1malloc

2.2calloc

2.3relloc

2.4free

2.5罗列常见的内存管理错误

三,C++内存管理方法

3.1new/delete管理体系

3.1.1开辟单个空间与释放

3.1.2开辟多个连续的空间与释放

3.2C+++针对自定义类型的内存管理

3.2.1调用构造的重要性

3.2.2对象开辟空间的方法

3.3C++内存开辟失败与抛异常

插曲:摩尔定律

3.4C++内存开辟的底层逻辑

3.4.1operator new 与 operator delete 函数(重要点进行讲解)

3.4.1.1operator new

3.4.1.2operator delete

3.4.2new和delete的实现原理

3.4.2.1内置类型

3.4.2.2自定义类型

new的原理

delete的原理

new T[N]的原理

delete[]的原理

3.4.3从汇编代码中看底层调用操作

new的底层

delete的底层

3.5C/C++内存开辟的各种错乱情况

3.5.1C/C++混用

3.5.2new/delete多对单

3.6总结malloc/free 和 new/delete 的区别(面试常考)

3.7placement-new

3.8拆分使用new


0.1概要&序論

         这里是此方,久しぶりです!。本文内容极长将详细介绍C语言内存管理包括:malloc、calloc、relloc、free、常见内存管理错误等内容和C++内存管理包括:new、delete以及他们的底层原理,最后会总结C/C++内存管理的区别。内容干货极其丰富!这里是「此方」。让我们现在开始吧!

一,布局模型与常见误区解析

1.1C/C++内存布局

C++的内存管理和c语言一致,以下是一图流分析其内存布局:

    我们来简要说明一下:

    1. 栈又叫堆栈,存放非静态局部变量/函数参数/返回值等等,栈是向下增长的。
    2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(暂时不用管,现在只需要了解一下)
    3. 堆用于程序运行时动态内存分配,堆是可以上增长的。(本文主要介绍)
    4. 数据段(俗称静态区)--存储全局数据和静态数据。
    5. 代码段--可执行的代码/只读常量

    我们可以这么理解:某种程度上说,分区分的是生命周期。

    栈内存在栈帧销毁时销毁。静态区内存一直到程序运行结束。堆内存的如果不去free/delete一直都在。

    因此:实际上需要我们程序员管理的内存——只有堆区内存。

    1.2内存布局易误解点

    题目:

    char2在哪里?____*char2在哪里?____pChar3在哪里?____*pChar3在哪里?____

    答案:栈,栈,栈,代码段(最后一个是不是猜错了doge)

    解释:

    • char2代表该数组的首元素指针存放在栈中很合理。
    • “abcd”只读字符串在代码段中,char2[]数组将该字符串拷贝一份到栈区中,*char自然指向被拷贝在在栈上的那个数组的首元素。
    • pChar3是指向“abcd”的指针,存放在栈中合理。
    • 但pchar3并没有像char2一样拷贝一份字符串到栈中,所以解引用的结果自然就在代码段上。

    二,复习C语言的内存管理方法

    头文件:<stdlib.h>

    2.1malloc

    C 语言提供了一个动态内存开辟的函数:

    void* malloc(size_t size);
    1. 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
    2. 如果开辟成功,则返回一个指向开辟好空间的指针。
    3. 如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查。
    4. 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候由使用者自己来决定。
    5. 如果参数 size 为 0,malloc 的行为在标准中是未定义的,取决于编译器。

    一个鲜明的例子:

    int* ret = (int*) malloc ( sizeof(int) * 10 ) ; if (ret == NULL){ perror ( "malloc fail" ); exit (1); } 

    2.2calloc

    C 语言还提供了一个函数叫 calloc,calloc 函数也用来动态内存分配。原型如下:

    void* calloc(size_t num, size_t size);
    1. 函数的功能是为 num 个大小为 size 的元素开辟一块空间并把每个字节初始化为 0。
    2. 与函数 malloc 的区别只在于初始化。

    一个鲜明的例子:

    int *p = (int*)calloc(10, sizeof(int)); if(NULL != p){ for(int i=0; i<10; i++){ printf("%d ", *(p+i)); } }

    2.3relloc

    realloc 函数的出现让动态内存管理更加灵活。
             有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

    函数原型如下:

    void* realloc(void* ptr, size_t size);
    1. ptr 是要调整的内存地址
    2. size 调整之后新大小
    3. 返回值为调整之后的内存起始位置。
    4. 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

    realloc 在调整内存空间的是存在两种情况:

    • 情况1:原有空间之后有足够大的空间
    • 情况2:原有空间之后没有足够大的空间

    1. 当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
    2. 当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

    2.4free

    C 语言提供了一个函数 free,专门是用来做动态内存的释放和回收的,函数原型如下:

    void free(void* ptr);
    1. free 函数用来释放动态开辟的内存。
    2. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。
    3. 如果参数 ptr 是 NULL 指针,则函数什么事都不做。

    2.5罗列常见的内存管理错误

    1. 对 NULL 指针的解引用操作。
    2. 对动态开辟空间的越界访问。
    3. 对非动态开辟内存使用 free 释放。
    4. 使用 free 释放一块动态开辟内存的一部分(这个很容易忘记)
    5. 对同一块动态内存多次释放。
    6. 动态开辟内存忘记释放(内存泄漏)(这个很容易发生)

    三,C++内存管理方法

    3.1new/delete管理体系

    3.1.1开辟单个空间与释放

    void Test(){ int * ptr = new int; delete ptr; }

    new开辟了int大小的空间给ptr。并delete销毁它。整个过程相对于C语言非常简单。

    1. new:开辟空间。——对标malloc
    2. delete:释放空间。——对标free

    我们也可以在开辟空间的时候初始化:如下:对标calloc,为ptr开辟空间的时候并同时初始化为1.

    int * ptr = new int (1); 

    3.1.2开辟多个连续的空间与释放

    void Test(){ int * ptr = new int[10]; delete [] ptr; }

            如上,我们开辟了十个int大小的空间给ptr指针,让其指向这个10*sizeof(int)大小的数组。值得注意的是,在delete释放内存的时候要加上[]。

           同样的,我们可以在开辟空间的时候初始化:

    int * ptr = new int[5]{1,2,3,4,5}; int * ptr = new int[10]{1,2,3,4,5}; int * ptr = new int[10]{0};

    结合上面的代码,初始化一共有三种情况:

    1. 完全初始化。按照你初始化的来。
    2. 部分初始化,剩下未初始化是部分默认是0。
    3. 直接给0,全部初始化为0。

    C++搞出来这个不是单纯是为了优化C语言那一套的写法。以上都是表层的内容。接下来深入讲解

    3.2C+++针对自定义类型的内存管理

             对于自定义类型,C++在申请空间的时候会自动调用构造函数,释放空间的是后会自动调用析构函数。

             演示代码:如图,我们new了10个A类类型的空间。并delete释放。于是构造函数和析构函数就被调用了10次。

    3.2.1调用构造的重要性

           我们来看一段链表代码:在如下代码中,我们创建了多个ListNode节点,并通过new动态分配内存:

    struct ListNode{ int val; ListNode* next; ListNode(int x) : val(x) , next(nullptr) {} }; int main(){ A* p1 = new A; A* p2 = new A(1); delete p1; delete p2; ListNode* n1 = new ListNode(1); ListNode* n2 = new ListNode(1); ListNode* n3 = new ListNode(1); ListNode* n4 = new ListNode(1); n1->next = n2; n2->next = n3; n3->next = n4; }

    同样的,发生了两个步骤:

    1. 分配内存:为ListNode类型的对象分配堆空间;
    2. 调用构造函数:自动执行构造函数初始化成员变量。
    若没有构造函数的自动调用:val可能是未定义值(如垃圾数据);next可能指向随机地址,导致后续访问崩溃;链表连接毫无逻辑,引发严重运行时错误。

    因此new自动调用构造函数,确保了每个节点在创建时就处于已知的状态

    3.2.2对象开辟空间的方法

    1,有默认构造函数并开辟一个对象大小空间

    A* p1 = new A; 

    2,有默认构造函数并开辟多个对象大小空间

    A* p3 = new A[3];

    3,没默认构造函数并开辟一个对象大小空间

    A* p2 = new A(2, 2);

    4,拷贝构造多个对象大小空间(最基础版

    A aa1(1, 1); A aa2(2, 2); A aa3(3, 3); A* p3 = new A[3]{aa1, aa2, aa3};

    5,拷贝构造多个对象大小空间(使用匿名对象版编译器会自己优化。

    A* p4 = new A[3]{ A(1,1), A(2,2), A(3,3) };

    6,拷贝构造多个对象大小空间(使用C++11特性版多参数构造隐式类型转换。

    A* p5 = new A[3]{ {1,1}, {2,2}, {3,3} };

    3.3C++内存开辟失败与抛异常

              一般情况下我们不再使用C语言的malloc那一套方法。为什么我们以前要写perror那一套检查法,而C++没有?C++我们引入了一种更加先进的方法:C++异常我会在C++进阶会详细讲

              malloc失败后返回空,new失败后不是返回空而是抛异常。(我们在C++检查返回值是没有用的),内存开辟失败并不常见,先造一个简易的失败模拟器:

              如图,弹窗提示开辟内存失败。(开辟内存太多而无法实现),现在出现了这种异常,在C++中,我们要尝试并捕获这种异常

    try { void* pa = new char[1024 * n]; } catch (const exception& e) { }
    • "try{ }catch"这一套用来捕获异常。
    • exception是标准库异常类类型,不可修改。
    • e是异常类型变量。
    try { void* pa = new char[1024 * n]; } catch (const exception& e) { cout << e.what() << endl; }

             如上代码,捕获异常后,我们还要知道到底发生了什么回事:异常类型变量e调用what()函数可以帮助我们。

               如上,what()函数返回异常信息"内存申请失败:bad allocation",说明我们异常捕获成功,并没有出现弹窗。

    插曲:摩尔定律

             英特尔(Intel)联合创始人之一戈登·摩尔(Gordon Moore)在 1965年 提出的一个经验性预测,它描述了半导体技术发展速度的一个趋势。摩尔最初观察到,集成电路上可容纳的晶体管数量大约每 18到24个月 便会增加一倍,同时成本保持不变。后来,这个定律常被引申为:

    集成电路(IC)上的晶体管数量大约每两年翻一番,性能也随之提升一倍。

            最初,我们使用的32位计算机,它的内存空间是2^32字节,也就是大约4GB。但是现在的64位计算机他的内存来到了2^64字节。2^64字节 = 18446744073709551616 字节(18万4467亿)

    特性32位系统 (典型例子)64位系统
    总虚拟地址空间4GB左右18,400,000,000 GB
    内核空间划分例如 1GB (固定或可选2GB)例如 128TB (或更大,非固定比例)
    用户空间划分例如 3GB (或2GB),其中大部分用于堆等例如 128TB (或更大),堆等用户内存从中分配
    堆大小限制受限于总用户空间 (约 2-3GB)受限于进程虚拟地址空间上限和物理内存,远超32位限制

    上面介绍了C++内存开辟的所有使用方法,但是知道这些显然不够,让我们深入底层再探讨探讨

    3.4C++内存开辟的底层逻辑

    3.4.1operator new 与 operator delete 函数(重要点进行讲解)

            new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数(虽然有operator,但是他们不是任何一个函数的函数重载),new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。

    3.4.1.1operator new

    看看这个函数的底层实现:

    void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void *p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); }

            该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施。如果改应对措施用户设置了,则继续申请,否则抛异常

    3.4.1.2operator delete

    operator delete: 该函数最终是通过free来释放空间的,为什么这么说?看底层实现。

    这是operator delete的底层:

    void operator delete(void *pUserData) { _CrtMemBlockHeader * pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg( pUserData, pHead->nBlockUse ); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; }

           这一句:_free_dbg( pUserData, pHead->nBlockUse );delete正在调用函数_free_dbg来释放空间。
    我们再看看free的底层:

    #define free(p) _free_dbg(p, _NORMAL_BLOCK)

              宏定义_free_dbg这个函数为free()。所以free本质上就是_free_dbg,而delete调用了它。C++的设计师在设计他的时候沿用了C语言的底层原理。

    3.4.2new和delete的实现原理

    3.4.2.1内置类型

    如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不会调用构造析构函数。不同的地方是:

    1. new/ delete申请和释放的是单个元素的空间,new[ ]和delete[ ]申请和释放的是连续空间,
    2. new在申请空间失败时会抛异常,malloc会返回NULL
    3.4.2.2自定义类型
    new的原理
    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的构造
            new开空间的时候是不会自己搞一套开空间的方法,它会去调用malloc,但是它不会自己去调用那一套C的malloc,而是一套包装形态的malloc:即:opreator new。原因也很简单,为了C++的异常那一套操作。
    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来释放空间

    3.4.3从汇编代码中看底层调用操作

    我们用来测试的类Asser:

    class Asser{ public: Asser(int x = 0, int y = 1) :_x(x) ,_y(y) {} ~Asser(){ _x = 0; _y = 0; } private: int _x; int _y; };
    new的底层

       如上图,我们在为str开辟Asser大小的空间时,先后调用了operator new函数和构造函数

    delete的底层

               如上图,delete底层调用了Asser::`scalar deleting destructor' (07FF6EFA71055h)  ,但是实际上,这个函数是operator delete函数和析构函数的包装

    3.5C/C++内存开辟的各种错乱情况

    3.5.1C/C++混用

    int main() { int* ptr=new int ; free(ptr); return 0; }

    程序会不会i崩溃?会不会内存泄漏?

    先说结论:不会崩溃也不会内存泄漏。

    内置类型不涉及构造和析构,没有调用构造函数的内置类型不会发生内存泄漏。这里new等同于malloc调用free调用的是free_dbg,delete也是调用free_dbg,free等同于delete。

             对内置类型可以这么写,但是不建议你这么写。但是自定义类型你不能这么搞,用free你少调用了一个析构函数。如果自定义类型的析构函数释放了xxx内存,那么这个时候就会出现内存泄漏。但是程序不会崩溃。

    3.5.2new/delete多对单

    int main() { Asser* ptr =new Asser[10]; delete ptr; return 0; } 

    先说结论:可能崩溃也可能内存泄漏。

            内存泄漏原理:new int [10]开辟一整块空间并调用10次构造函数。delete[] 释放一整块空间。但是只调用了一次析构函数。如果类内部申请了空间,那么这些空间就有可能被内存泄漏。

            崩溃原理:释放空间时释放内存的一部分。(有点复杂,下面详细分析)

             如上图,我们运行程序,确实崩溃了。转到反汇编并监视:size:

               我们看到,实际上new调用operator new开辟空间的时候开辟了88个字节的空间而不是80个字节。这多出来的8个字节空间是用来存放“开了多少个对象的”。

             如上图,一目了然,空间开辟后我们的ptr指针指向的是80个字节的开始,而不是整个被开辟空间的开始,这个就导致了delete的时候我们不能完全释放掉所有的空间。导致崩溃。

    但是值得注意的是,内置类型不会崩溃:

    int* str = new int[10]; delete str;

             因为内置类型事实上我们就开辟了40个字节的空间,并没有额外的4字节空间来存放“开了多少个对象”,也就没有了从中间释放空间的情况。

             实际上自定义类型有这个额外空间而内置类型没有的本质是因为delete[]str,这个空间存放的“开了多少个对象”的这个数值,是在编译的时候给[]用的。内置类型既然没有析构函数,也就不需要采用额外开辟空间来存放析构函数的调用个数

    3.6总结malloc/free 和 new/delete 的区别(面试常考)

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

    1. malloc 和 free 是函数,new 和 delete 是操作符
    2. malloc 申请的空间不会初始化,new 可以初始化
    3. malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可,如果是多个对象,[] 中指定对象个数即可
    4. malloc 的返回值为 void*,在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型
    5. malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常
    6. 申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理释放

    3.7placement-new

    定位 new (placement new) 是 C++ 中new操作符的一种特殊形式。它并不分配新的内存,而是允许你在已经分配好的内存中创建一个对象。

    它的基本语法如下:

    new (place_address) type new (place_address) type(initializer_list)
    • place_address: 这是一个指针,指向一块你预先已经分配好的、足够容纳 type 类型对象的内存空间。
    • type: 你想要在该内存中创建的对象的类型。
    • initializer_list: 用于初始化新创建对象的参数列表。

    3.8拆分使用new

    说白了就是显式调用构造和析构函数初始化/释放。

    int main(){ A* p1 = new A(1); delete p1; A* p2 = (A*)operator new(sizeof(A)); new(p2)A(1); p2->~A(); operator delete(p2); return 0; }

             以上两套是一回事,看到这里,读者一定会想,“这不是脱裤子放屁吗?”

              没错,实际上,拆分使用new在99%的场景下是用不到的。在极少数的情况下,会有用,如“池化技术中的内存池”(这个以后会讲)。

    剩下一些内存泄漏的规则,我们暂时不谈,因为没法讲,我放到了指针指针再说。


            好的,本期内容就到这里,如果对你有帮助,还不要忘了点赞三联一波哦,我是此方,我们下期再见。

    Read more

    个人所得税的APP模拟器,纯java版代码开源,截图录屏都可以【仅供参考】

    个人所得税的APP模拟器,纯java版代码开源,截图录屏都可以【仅供参考】

    文件下载地址:https://wenshushu.vip/pan/index.php?id=36    提取码:7bf9 给大家分享一个用纯Java实现的个人所得税计算模拟器,包含完整的GUI界面和核心计算逻辑,适合Java学习者和税务计算需求者参考使用。 一、项目简介 这是一个使用Java Swing开发的个人所得税计算模拟器,模拟了官方个税APP的核心功能,包括: · 综合所得年度汇算计算 · 税率表查询 · 专项扣除项目设置 · 税务计算结果展示 项目特点: · 100%纯Java实现,无第三方依赖 · 完整GUI界面,支持用户交互 · 详细的代码注释 · 遵循2023年最新个税政策 二、核心代码实现 1. 主程序入口 ```java package com.tax.calculator; import javax.swing.*; /**  * 个人所得税计算模拟器 - 主程序  * @author TaxDeveloper  * @version

    By Ne0inhk

    JavaScript性能优化实战:流畅应用秘籍

    一、性能优化的重要性 1. 用户体验的核心:流畅度与响应速度 2. 性能对业务指标的影响(转化率、留存率) 3. 现代 Web 应用的性能挑战 4. 本文目标:提供可落地的优化方案 二、性能瓶颈分析与度量 1. 关键性能指标 (Web Vitals) * LCP (Largest Contentful Paint):最大内容渲染时间 * FID (First Input Delay):首次输入延迟 * CLS (Cumulative Layout Shift):累积布局偏移 * 如何测量这些指标(Chrome DevTools, Lighthouse, Web Vitals API) 2. 浏览器开发者工具剖析 * Performance 面板:记录和分析运行时性能 * Network

    By Ne0inhk
    Elasticsearch核心概念与Java客户端实战 构建高性能搜索服务

    Elasticsearch核心概念与Java客户端实战 构建高性能搜索服务

    目录 🎯 先说说我被ES"虐惨"的经历 ✨ 摘要 1. 为什么选择Elasticsearch? 1.1 从数据库的痛苦说起 1.2 Elasticsearch的优势 2. ES核心架构解析 2.1 集群架构 2.2 索引与分片 3. Java客户端实战 3.1 客户端选型对比 3.2 RestHighLevelClient配置 3.3 Spring Data Elasticsearch配置 4. 索引设计最佳实践 4.1 索引生命周期管理 4.2 映射设计技巧 5. 查询优化实战 5.1 查询类型对比 5.

    By Ne0inhk
    【微服务】Java 对接飞书多维表格使用详解

    【微服务】Java 对接飞书多维表格使用详解

    目录 一、前言 二、前置操作 2.1 开通企业飞书账户 2.2 确保账户具备多维表操作权限 2.3 创建一张测试用的多维表 2.4 获取飞书开放平台文档 2.5 获取Java SDK 三、应用App相关操作 3.1 创建应用过程 3.2 应用发布过程 3.3 应用添加操作权限 四、多维表应用授权操作 五、使用控制台API调试操作多维表 5.1 控制台调试多维表操作过程 5.1.1 获取token 5.1.2 获取多维表数据 5.1.3

    By Ne0inhk