• 深入篇【C++】C&C++内存管理:new/delete底层原理剖析+思维导图总结



    在这里插入图片描述

    Ⅰ.C/C++内存分布

    程序中需要存储不同的数据,这些数据大致分为以下几种:局部数据,静态数据和全局数据,常量数据,动态申请的数据。
    那这些数据都存在在哪里呢?
    在这里插入图片描述
    我们来分析一下下面这段代码的相关问题。

    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__
       staticVar在哪里?_C___   localVar在哪里?_A___
       num1 在哪里?__A__
       
       char2在哪里?_A___   *char2在哪里?__A_
       pChar3在哪里?__A__      *pChar3在哪里?_D___
       ptr1在哪里?_A___        *ptr1在哪里?_B___
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    总结
    1.栈又叫做堆栈–是存储非静态局部变量(如函数参数,函数返回值,局部变量等),栈是可以向下增长的。
    2.堆用于存储动态申请的数据,堆是可以向上增长的。
    3.静态区/数据段是存储全局变量和静态变量的。
    4.代码段是存储可执行的代码的只读常量。

    Ⅱ.C的内存管理

    C语言中用于动态内存管理方式有:malloc / calloc /realloc /free

    int main()
    {
    	int* a1 = (int*)malloc(sizeof(int));
    	free(a1);
    
    	int* a2 = (int*)calloc(4,sizeof(int));
    
    	int* a3 = (int*)realloc(a2, sizeof(int) * 5);
    	//a2还需要释放嘛?
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    malloc / calloc /realloc它们的作用分别是什么呢?
    maollc用来动态开辟一块大小为sizeof(类型)的空间,但不初始化。
    calloc用来动态开辟一个n*sizeof(类型)的空间,并全初始化为0。
    realloc用来扩容,扩容的方式有两种:
    一种是原地扩容,当原来空间的后面有足够的空间可以申请那就在原来空间的基础上往后扩容。
    第二种是异地扩容,当原来空间后面没有足够的空间可以申请,那就在另一块空间申请,会将原来的空间释放。
    所以上面a2是不用释放的,因为当原地扩容,a2的空间就是a3正在使用的不能释放,当异地扩容是系统已经帮你释放了。

    Ⅲ.C++的内存管理

    C语言中的内存管理方式在C++中仍可以继续使用,但有些地方就无能为力了,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

    ①.new/delete操作内置类型

    int main()
    {
        //动态申请一个int类型的空间
    	int* p3 = (int*)malloc(sizeof(int));//C
    	free(p3);
    	//动态申请一个10大小的int的类型的数组
    	A* p1 = (A*)malloc(sizeof(A) * 10);//C
    	free(p1);
    	
    	//动态申请一个int类型的空间
    	int* p4 = new int;//C++
    	delete p4;
    	
    	//动态申请一个int类型的空间,并初始化。
    	int* p5 = new int(1);//C++
    	delete p5;
    
    	//动态申请一个10大小的int的类型的数组
    	A* p2 = new A[10];//C++
    	delete[] p2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    C++中内存管理的方式是:new与delete,注意它们是操作符不是函数。
    new后面加类型,就是动态申请一个这个类型的大小的空间。
    new后面加类型加圆括号,就是动态申请一个这个类型大小的空间并初始化。
    new后面加类型加方括号,就是动态申请n个该类型的大小的空间。
    而要释放资源就直接在delete后面加上该类型即可,不过当释放的资源是连续的时,就需要使用delete[ ]来释放。

    从上面的使用来看,new/malloc除了用法上不同,其他方面没有上面区别的。但唯一要注意的就是new可以给变量进行初始化,而malloc不可以。
    在这里插入图片描述
    相比较C/C++对于内置类型动态内存的管理,我们发现C++对于内置类型使用上更方便。

    总结:

    1.申请和释放单个空间,用new和delete。
    2.申请和释放连续的空间,用new[ ]和delete[ ]。
    3.要注意匹配,不能乱搞。

    ②.new/delete操作自定义类型

    对于自定义类型,C/C++是如何进行内存管理的呢?

    class A
    {
    public:
    	explicit A(int a = 0)
    		:_a(a)
    	{
    
    	}
    	A(const A& a1)
    	{
    		_a = a1._a;
    	}
    private:
    	int _a;
    };
    int main()
    {
    	//动态开辟一个A类型的空间
    	A* aa1 = (A*)malloc(sizeof(A));//C
    	A* aa2 = new A(1);//C++
    	free(aa1);
    	delete aa2;
    
    	//动态开辟一个10个A类型大小的数组
    	A* aa3 = (A*)malloc(sizeof(A) * 10);//C
    	A* aa4 = new A[10];//C++
    	free(aa3);
    	delete[] aa4;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    对于自定义类型跟内置类型的处理方式是一致的。
    但new/delete和malloc和free的最大区别在于new/detele对于【自定义类型】除了开空间,还会自动调用构造函数和析构函数。
    在这里插入图片描述
    在这里插入图片描述

    所以如果动态开辟10个A类型大小的数组,就相当于创建了10个对象,会调用10次构造函数,这里的构造函数是默认构造函数,当没有默认构造函数时,就必须自己初始了。
    我们可以这样初始化:

    	A* aa4 = new A[10]{0123456789};
    	//当构造函数参数为两个时,可以这样初始化
    	B* bb1=new B[4]{B(1,2),B(2,3),B(4,5),B(6,7)};
    	B* bb1=new B[4]{B(1,2),B(2,3),B(4,5)};
    	但注意这样是不行的,创建了4个对象,你手动初始化了3个,还有一个说:你看不起谁呢!
    	这种必须让构造函数变成默认构造函数(全缺省形式)的才可以。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    struct ListNode
    {
    	int _val;
    	struct ListNode* _next;
    
    	ListNode(int x=0)
    		:_val(x)
    		,_next(NULL)
    	{}
    };
    struct ListNode* BuyListNode(int x)
    {
    	//C  malloc单纯的开空间,还需要自己转化类型
    	struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
    	//还需要检查
    	if (newnode == NULL)
    	{
    		perror("malloc");
    	}
    	//需要开完空间自己手动初始化
    	newnode->_next = NULL;
    	newnode->_val = x;
    	return newnode;
    }
    int main()
    {
    	struct ListNode* n1 = BuyListNode(1);
    	struct ListNode* n2 = BuyListNode(2);
    
    	//但C++的new就不一样了,new是开完空间后还可以自己初始化
    	//怎么初始化的呢?会调用构造函数初始化
    	struct ListNode* n3 = new ListNode(6);
    	struct ListNode* n4 = new ListNode;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    在这里插入图片描述

    总结:

    动态申请自定义类型的数据new和malloc除了使用方式上有点区别,还有一个重大区别:new在申请空间时会调用构造函数初始化,malloc在申请空间时不会去调用构造函数,不能初始化。

    ③.operator new与operator delete

    new和delete是C++进行动态内存申请的操作符,operator new 和operator delete是系统提供的全局函数,new在底层会去调用operator new来申请空间,delete在底层会去调用operator delete来释放空间。
    在这里插入图片描述

    malloc就是用来申请空间的,那是不是new在使用时就调用了malloc来申请空间呢?
    其实不是这样,new在申请空间时,本质上调用的是operator new这个全局函数来申请空间,而operator new 这个全局函数里面才是真正的去调用malloc申请空间的。

    //operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
    //尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
    */
    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);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可能会有点迷惑,为什么new不直接去调用malloc呢?还非要去调用operator new,operator new 里面才去使用malloc。真是莫名其妙哈。但其实有设计这样的理由的。
    面向对象语言在处理失败时不喜欢用返回值,而更建议用抛异常。
    而我们知道malloc在申请空间失败时返回的是空,C++不想使用这个,从而设计出一个operator new来,因为在operator new函数里,使用了malloc并且如果当申请空间失败时就会抛出异常,而不是返回空。
    所以new在空间时,不能直接调用malloc,而是通过operator new函数来调用malloc。

    同理delete 会先调用析构函数,再去调用operator delete释放空间。
    而operator delete函数里又使用了free。

    /*
    operator delete: 该函数最终是通过free来释放空间的
    */
    /*
    free的实现
    */
    #define   free(p)               _free_dbg(p, _NORMAL_BLOCK)
    我们观察free在operator中是被使用的。
    
    
    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 );
             //调用free函数
         __FINALLY
             _munlock(_HEAP_LOCK);  /* release other threads */
         __END_TRY_FINALLY
         return;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    总结:

    通过上面两个全局函数我们可以知道,operator new其实也是通过malloc来申请空间的,如果申请成功就直接返回,否则就执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常,同理operator delete最终也是通过free来实现的。从这里我们可以知道operator new 函数和operator delete函数 是为了new 和delde操作符准备的。

    ④.new/delete底层实现原理

    对于内置类型:
    new和malloc ,delete和free基本类似,不同的地方是new/delete是申请单个空间。
    new[ ]/delete[ ]申请的是连续的空间,new在申请空间失败时会抛异常,malloc在申请空间失败时会返回NULL。

    对于自定义类型


    • 1.调用operator new函数申请空间。
      2.在申请的空间上调用构造函数初始化。


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


    • 1.调用operator new[ ]函数,operator new[ ]函数本质还是去调用operator new函数申请N个对象的空间。
      2.在申请的空间上执行N次构造函数。


    • 1.在释放的对象空间执行N次析构函数,完成N个对象的资源清理工作。
      2.调用operator delete[ ]函数,operator delete[ ]函数本质还是去调用operator delete函数释放对象空间。

    总结:

    malloc/free和new/delete的区别是什么?
    相同点:都是用来动态申请空间的,都是在堆上开辟,都需要用户手动释放空间。
    不同点:
    1.对于自定义类型在使用时,malloc只是申请空间,不会去调用构造函数,free也只是释放对象空间,而new除了申请空间外还会调用构造函数。delete在释放空间之前会先调用析构函数。

    2.new/delete 是操作符不是函数,malloc/free是函数。

    3.new/delete申请/释放失败会抛异常,malloc/free申请/释放失败是会返回NULL

    4.malloc申请的空间不能初始化,new申请空间可以初始化。

    5.malloc在申请空间时需要手动计算空间大小并传递,new在申请空间时只需加上类型即可,如果申请是多个对象,那只需在[ ]里写上对象个数即可。

    6.malloc的返回值是void*,在使用时必须进行强转,而new不需要,因为new后面就跟着使用的类型。

    ⑤.定位new表达式(placement-new)

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

    我们知道,new底层实现的原理是先申请空间,再调用构造函数,这个构造函数是自动调用的,而这里的定位new需要我们显式的调用构造函数。
    【使用格式】:
    new(place_address)type或者new(place_address)type(initializer-list).
    这里place_address表示已经分配的原始内存空间的地址,必须是指针。
    initializer-list是初始化列表。
    【使用场景】:内存池
    当需要频繁的申请和释放内存时,就是每次都要在堆上申请空间,会觉得很烦,所以有人就搞出一个内存池的概念,从堆上提前要些空间放进内存池中,当以后要用申请空间时,就可以直接从内存池里获取。不需要再到堆上申请,但内存池上的空间是没有初始化的。
    所以定位new表达式在时间中一般是配合内存池使用,因为内存池分配的内存没有初始化,所以如果是自定义类型的对象,需要使用new定义表达式来显式调用构造函数来初始化对象。
    在这里插入图片描述

    class A
    {
    public:
    	explicit A(int a = 0)
    		:_a(a)
    	{
    
    	}
    	A(const A& a1)
    	{
    		_a = a1._a;
    	}
    	~A()
    	{
    		cout << "~A()" << endl;
       }
    private:
    	int _a;
    };
    
    int main()
    {
    	//aa1可不是对象,aa1现在只不过是指向与A对象一样大的空间,不能算作对象,因为构造函数没有执行,没有实例化。
    	A* aa1 = (A*)malloc(sizeof(A));
    	
    	new(aa1)A;//显式的调用构造函数,要注意如果A的构造函数有参数需要传参。
    
    	aa1->~A();//手动的调用析构函数
    
    	free(aa1);//手动的调用free
    
    	//同理如下
    	A* aa2 = (A*)operator new(sizeof(A));
    	new(aa2)A(10);
    	aa2->~A();
    	free(aa2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    在这里插入图片描述

    总结:

    1.定位new表达式是具有价值的,在内存池应用方面很有用,因为从内存池分配的内存没有初始化,需要我们显式的去调用构造函数来初始化对象。
    2.池化技术是用来提高效率的。
    3.定位new就是在模拟new。

  • 相关阅读:
    mysql5.7获取json数组中的某个对象
    git使用,一点点
    Atcoder Beginner Contest 273E - Notebook 解题报告
    python面试题(36~50)
    Web服务无法响应但本地业务正常的故障排查记录
    基于SSM+Vue的校园活动管理平台设计与实现
    【AI】Python 实现 KNN 手写数字识别
    Android 性能优化相关
    day2-web安全漏洞攻防-基础-弱口令、HTML注入(米斯特web渗透测试)
    实现了Spring的Aware接口的自定义类什么时候执行的?
  • 原文地址:https://blog.csdn.net/Extreme_wei/article/details/130910360