• 【C++】动态内存管理


    在之前类和对象的博客里面,已经记录过了C++中动态内存管理函数newdelete的基本使用。本篇博客是对C++动态内存管理的进一步细化

    1.C/C++内存分区

    这是一个老生常谈的问题了,直接看下面这个图吧!

    image-20220624163644747

    这里的数据区其实就是静态区,而代码区是常量区。这里的BBS区先暂时pass掉。

    要想辨别上面的几个内存分区,可以现来看下面这个代码,你能分的清楚它们都是存在内存的哪一个区域吗?

    int a = 1;//数据区
    static int b = 1;//数据区
    int main()
    {
         static int c = 1;//数据区
         int d = 1;//栈
    
         int arr1[10] = {1, 2, 3, 4};//栈
         char arr2[] = "abcd";//栈
         char* arr3 = "abcd";//"abcd"存在代码区
         int* ptr = (int*)malloc(sizeof (int)*4);//堆
         free (ptr);
        //其中,ptr指针本身是存在栈区的
        //同理,arr3指针本身存在栈区
        //但是arr3指针指向的对象是存在代码区(静态区)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.C++动态内存管理

    在C语言中,基本的动态内存管理通过malloc和free实现

         int* ptr = (int*)malloc(sizeof (int)*4);//堆
         free (ptr);
    
    • 1
    • 2

    在C++中,对应产生了new和delete,它们比前者更加高级,具有更多特性

    2.1基本认识new和delete

    下面是基本的使用方式,想必大家看了之后,是“有手就行”😂

    	int*p1=new int;//开辟一个int类型的空间
        int*p2=new int(10);//开辟一个int类型的空间,并初始化为10
        int*p3=new int[10];//开辟10个int类型的空间
        //注意后两个的括号区别!
        
        delete p1;//销毁p1指向的单个空间
        delete p2;//同上
        
        //delete p3;//销毁p3指向的第一个空间,不能用于数组
        delete[] p3;//销毁p3指向的数组
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.2操作类对象

    new相比于malloc,最大的区别在于处理自定义类型的时候。类和对象就是C++中与C语言完全不同的自定义类型。

    我们知道,当你使用类名创建一个对象的时候,编译器会自动调用这个对象的构造函数。那如果我们用new来创建一个自定义类型的对象呢?

    class Stack{
    private:
        int* _a;
    }
    int main()
    {
        Stack* p1=(Stack*)malloc(sizeof(Stack));
        Stack* p2=new Stack;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这时候的区别就在于

    • new在创建的对象的时候,会自动调用该对象的构造函数
    • malloc在创建对象的时候,不会调用构造函数

    这样就能解释,为什么C++要单独弄出一个new,而不是继续沿用C语言的malloc了。因为我们在class中定义成员变量的时候,大多数是定义成私有的。如果对象在创建的时候没有进行构造,我们很难从外部访问类内部的私有成员进行初始化操作。

    所以new的出现,让我们能够在堆上开辟对象空间的同时,初始化这个对象

    不难理解,delete和free的区别也是如此:

    • 当你调用delete的时候,编译器会调用类的析构函数
    • 使用free不会调用析构函数,可能造成内存泄漏

    image-20220624202643155

    2.3对象数组

    // 申请单个Test类型的对象
     Test* p1 = new Test;
     delete p1;
     
     // 申请10个Test类型的对象
     Test* p2 = new Test[10];
     delete[] p2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    和内置类型一样,我们也可以方便的使用new来实现开辟对象数组

    注意,在delete操作的时候,一定要注意匹配问题,不能直接用delete p2来释放开辟的数组空间


    2.4给构造函数传参

    如果这个类的构造函数是包含参数的话,还可以使用下面这种方式在开辟空间,调用构造函数时传参(注意括号区别)

     Test* p3 = new Test(10);//给对象Test的构造函数传参
     delete p3;
    
    • 1
    • 2

    image-20220624202949406

    3.operator new/delete函数

    看到这个名字,估计你和我一样,会下意识的认为这个是c++中对new和delete操作符的重载。nope!这两个实际上是C++中实现new和delete的一部分函数

    为啥说是一部分呢,让我们来康康它的源码

    operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
    {
      void *p;
      /* malloc (0) is unpredictable; avoid it.  */
      if (sz == 0)
        sz = 1;
    
      while (__builtin_expect ((p = malloc (sz)) == 0, false))
        {
          new_handler handler = std::get_new_handler ();
          if (! handler)
        _GLIBCXX_THROW_OR_ABORT(bad_alloc());
          handler ();
        }
    
      return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    你回复下,这个函数最终使用了malloc来开辟空间,只是在这之上,new还引入了抛异常机制

    • C语言中,malloc错误会返回null指针
    • C++中,new错误会执行抛bad_alloc异常操作

    不知抛异常是什么?我们可以暂且不用理解它。只需要知道,当new失败的时候,控制台会直接报错终止程序,而不是和malloc一样,将指针变空指针,从而导致可能出现的解引用空指针操作

    实际上,当我们new一个对象的时候,会执行下面两个函数

    operator new
    对象的构造函数
    
    • 1
    • 2

    在VS中打开调试,转到反汇编,你便可以看到编译器call这两个函数的操作

    image-20220625101016238

    再来看看operator delete的代码

    https://cplusplus.com/reference/new/operator%20delete/

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们发现,delete最终也是通过调用free实现的

    image-20220625101848948

    但是这里的反汇编,我就有点看不懂了。看起来也调用了一个Stack函数,姑且认为那个就是Stack的析构函数吧


    3.1 new和delete的实现原理

    看到这里,让我们来总结一下new和delete的实现原理。

    new的原理:

    • 调用opeartor new申请空间,错误时抛异常
    • 如果操作的是自定义类型,则调用构造函数

    new int[N]原理:

    • 调用operator new[](没错库里面还有一个这个函数)
    • operator new[]中实际调用N个operator new来申请空间
    • 如果操作的是自定义类型,则还会分别调用N次构造函数

    delete的原理:

    • 在原始空间上调用析构函数,清楚内容
    • 调用operator delete执行对象空间的free操作

    delete int[N]的原理:

    • 调用N次析构函数
    • 调用operator delete[],实际调用N个operator delete来释放N个对象的空间

    4.定位new

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

    啊嘞,new不是会自己调用构造函数吗?这个定位new有是来干什么的?

    查阅了一些我现在看不懂的资料后,了解到,定位new的操作多半是配合自己写的内存池来进行操作。在之前博客中出现的Tcmalloc就是谷歌写的一个内存池

    4.1什么是内存池?

    当我们使用new或者malloc时,是通过编译器向操作系统申请空间

    而内存池就是一个我们写的预先申请内存空间的模块

    这个模块会在执行后,先预先向操作系统要一个相对较大的空间。我们后续的操作就是在这个已经开辟好的空间中再次申请空间来实现的

    因为这样就是从自己的口袋里面拿东西,没有中间商赚差价,效率就会提高不少

    但是这样就没有了new本身自动调用构造函数的优势,需要我们自己来调用构造函数

    4.2定位new的使用

    new(place_address) type
    或者
    new(place_address) type(initializer-list)
    
    • 1
    • 2
    • 3

    以下面这个类为例

    class Stack {
    public:
        Stack(int num = 5)
        {
            _a = new int[num];
            _capa = num;
            cout << "Stack(int)" << endl;
        }
    
        ~Stack()
        {
            delete[] _a;
            _capa = 0;
            cout << "~Stack()" << endl;
        }
    private:
        int* _a;
        int _capa;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们先使用malloc来模拟没有调用构造函数的情况,再使用定位new来调用构造函数

    int main()
    {
        Stack* p= (Stack*)malloc(sizeof(Stack));
    	new(p) Stack; //如果类的构造函数有参数时,此处需要传参
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到,编译器成功调用了构造函数

    image-20220625103737599


    5.更多知识点

    5.1 malloc和new的区别

    malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放

    不同的地方是:

    1. malloc和free是函数,new和delete是操作符
    2. malloc申请的空间不会初始化,new可以初始化(通过new int(10))
    3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可(其实就是省略了sizeof这一步的操作)
    4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
    5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
    6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中内存的清理

    5.2 内存泄漏

    我们知道,当堆区申请的空间没有进行释放的时候,就会出现内存泄漏,造成内存的浪费,甚至导致操作系统boom!

    • 堆内存泄漏

    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存, 用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏

    • 系统资源导致的泄漏

    系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。

    除了忘记free或者delete之外,另外的一些情况导致程序提前终止,也会出现内存泄漏

    int main()
    {
        Stack* p1 = (Stack*)malloc(sizeof(Stack));
        Stack* p2 = new Stack;
        
        
        free(p1);
        return 1;//只是做个示例,实际上哪有人这么写代码啊!
        
        delete p2;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    比如上面这个函数中,free之后执行了return,跳过了delete的操作,即导致p2的内存没有被释放,出现了内存泄漏

    解决内存泄漏有很多办法,其中最好的办法就是维持一个良好的代码风格,避免出现忘记释放内存的情况!

    5.3 深拷贝

    这个麻烦大家移步之前类和对象的博客啦

    https://blog.csdn.net/muxuen/article/details/124881928?spm=1001.2014.3001.5501


    结语

    本篇博客到这里就结束了

    期末考试其实已经结束5天了,我还在摸鱼……呜呜

    QQ图片20220418131327

  • 相关阅读:
    【矩阵论】1.准备知识(下)
    vLLM-prefix浅析(System Prompt,大模型推理加速)
    爬虫技术抓取网站数据
    求链表的相交节点
    小程序根据经纬度导航
    代码随想录算法训练营第五十六天| 1143.最长公共子序列 、 1035.不相交的线 、53. 最大子序和 动态规划
    知识积累:PageHelper分页问题,页码小于总页数和大于总页数返回数据问题,PageHelper分页失效
    爬虫之数据解析
    httplib库的安装以及使用
    边缘计算、云计算、雾计算在物联网中的作用
  • 原文地址:https://blog.csdn.net/muxuen/article/details/125456615