• C/C++内存管理


    image.png
    本文主要是讲解一下C和C++在堆上开辟空间, malloc realloc calloc new delete …如何使用,如何避免内存泄漏

    📌C/C++进程地址空间

    我们来看看C/C++中程序的内存划分

    image.png

    1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
      束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
      分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
      回地址等。
    2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
      配方式类似于链表。
    3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
    4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

    我们平时在C语言学习的过程中定义变量,定义数组都是在栈上开辟,有很明显的缺陷,比如想开辟的空间较大,在栈上就非常的不合适,所以我们来看看C语言动态内存开辟和C++动态内存开辟的共同点和区别

    💛C语言动态内存开辟

    C语言提供了好几个动态内存开辟的函数,我们来看看

    📄malloc

    image.png

    这个函数就是在堆上申请空间, 参数size指的是开辟空间的字节大小
    如果申请成功,返回申请到的空间的首地址
    如果申请失败, 返回NULL

    返回的参数类型是void* ,因为它并不知道你要如何使用, 所以要强转成你想使用的指针类型
    image.png

    可以看到这样就申请一个int类型的空间

    我们在使用的时候,一般都是使用sizeof配合使用
    DataType* ptr = (DataType*) malloc (sizeof(DataType) * n );
    例如我们想要开一个10个整型的空间
    image.png

    这里要注意的是,malloc开辟的空间是一块连续的空间, 所以可以当做数组使用

    在很多编译器中,都需要检查malloc是否开辟成功,否则编译不通过,
    image.png

    总结一下malloc
    这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针

    • 如果开辟成功,则返回一个指向开辟好空间的指针。
    • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
    • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
    • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

    📃free

    我们使用malloc在堆上开辟空间了之后,如果我们不使用这块空间, 就要手动释放掉, 这就涉及到了c/c++很难避免的问题叫内存泄漏,那我们如何手动释放一块空间呢, C语言提供了free这个函数来释放

    image.png

    参数就是堆上开辟的地址,free对这块空间进行处理,但是free并不会把这个指针置空,所以我们一般都需要手动置空

    我们来使用一下这个函数
    image.png

    这里已经释放完了, 但是arr还是没有被置空, 但我们继续对arr解引用呢??
    大家可以试试, 当然这是学习指针的时候就应该注意的野指针问题了,所以一定要置空哦

    int main(void)
    {
    	int* arr = (int*)malloc(sizeof(int) * 10);
    	if (NULL == arr)
    	{
    		exit(-1); //表示退出程序
    	}
    	//使用空间
    
    	free(arr);
    	arr = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    📑calloc

    大家学习了malloc的使用之后,calloc就比较容易了

    image.png
    函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
    与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

    我们来试一下

    image.png

    📝realloc

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

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

    realloc开辟空间有两种情况

    一种是在原地进行扩容,另一种是原地空间不够了,在异地进行扩容

    image.png

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

    如果realloc第一个参数传入NULL,那么realloc的作用就相当于malloc
    image.png

    🍎常见的C动态内存错误

    🕐 对NULL的解引用操作

    对NULL指针进行解引用,读写操作都会造成空指针异常,

    image.png

    🕜对动态开辟空间的越界访问
    void test()
    {
    	int i = 0;
    	int* p = (int*)malloc(10 * sizeof(int));
    	if (NULL == p)
    	{
    		exit(-1);
    	}
    	for (i = 0; i <= 10; i++)
    	{
    		*(p + i) = i;//当i是10的时候越界访问
    	}
    	free(p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里我们在执行赋值之后并不会发生错误
    image.png

    可以看到这里没执行free之前是正常的,包括值都正常赋进去了, 为什么可以正常赋值呢?
    编译器并不会进行检查下标越界,不管是哪个C编译器一般都不会进行检查越界,因为下标可以作用于任意的指针,不止止是数组名,
    C的下标检查需要的开销太大了,编译器必须在程序中插入指令,证实下标表达式的结果所引用的元素和指针属于同一个数组,还需要存储数组的长度和位置…以及自动扩容和动态分配的又该如何, 会非常麻烦,时间和空间会有很多浪费,而且会影响指针的灵活性, 所以更多要求程序员本身注意不要越界

    所以这里可以正常赋值, 但是在free的时候就会程序奔溃,因为你想要释放本不属于你的空间image.png

    🕙对非动态开辟内存使用free释放

    这就本身属于滥用了,但是并不缺乏很多人去这样做,
    free函数本身功能就是释放动态开辟的内存, 你如果使用free释放在栈上的空间,就一定会出错

    void test()
    {
    	int a = 10;
    	int* p = &a;
    	free(p);
    }//大家可以自行尝试一下
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    🕥使用free释放一块动态开辟内存的一部分

    假如我们开辟了10个整型的空间,那我们想要变成5个,是否可以把后五个释放掉呢?
    当然是不可以的,要使用realloc

    void test()
    {
    	int* p = (int*)malloc(sizeof(int) * 10);
    	int* tmp = p;
    	tmp += 5;
    	free(tmp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image.png

    还有两个常见的错误是 对动态开辟的空间进行多次释放
    忘记释放,造成内存泄漏 , 这两个大家可以自行尝试一下

    🌐总结

    malloc 进行空间开辟
    calloc = malloc + memset
    realloc 动态增容

    一般我们需要使用初始化好的空间就用calloc
    我们需要使用动态增容, 就使用realloc
    一定要注意常见的内存错误, 申请的空间不用了一定要释放, 并且把指针置空,避免野指针

    💛C++动态开辟

    C++是面向对象的语言, 引入了类和对象,所以在C++中我们用对象来说,
    首先动态开辟的对象还是在堆上,C++是兼容C的,C语言的内存管理在C++中还可以使用,但是使用起来总是有些不便,而且有的问题解决不了, 所以C++通过new 和delete 进行动态内存管理

    ☝new/delete

    这个语法使用还是比较简单的

    void Test()
    {
        // 动态申请一个int类型的空间
        int* ptr4 = new int;
        // 动态申请一个int类型的空间并初始化为10
        int* ptr5 = new int(10); 
        // 动态申请10个int类型的空间
        int* ptr6 = new int[3]; //[]中是对象个数
        //单个对象使用delete
        delete ptr4;
        delete ptr5;
        //多个对象使用delete[]
        delete[] ptr6;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在C++中new开辟的数组也可以初始化

    image.png

    操作起来比较简单, 需要注意的就是new 和 delete需要配对使用, new[] 和 delete[]需要配对使用
    malloc和free也需要配对使用, 不要混着用, 可能会造成位置错误

    有些朋友就会问了,那么new和delete 到底和malloc和free有啥区别吗?
    image.png
    可以说对于 int /double …这种内置类型是一模一样的,但是对于自定义类型来说就不同了

    C++创建对象会调用默认构造函数, 对象生命周期结束时也会调用析构函数

    那么new和delete有没有调用构造函数和析构函数呢 ,我们来看看
    首先定义一个空类,只要构造和析构函数

    class A
    {
    public:
    	A()
    	{
    		cout << "A()" << endl;
    	}
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image.png

    所以new/delete 和 malloc/free最大的区别就是 new和delete 会调用对象的构造函数和析构函数

    ☝operator new 与 operator delete函数(new/delete的底层实现)

    operator new 和 operator delete 是系统提供的全局函数
    new和delete在底层调用的就是operator new 和 operator delete

    那么具体是怎么实现的呢,我们来瞅瞅

    🔹operator new

    首先来看一下反汇编

    image.png

    可以看到确实调用了opertor new 和 构造函数, 那么operator new 函数是怎么实现的呢?

    void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
    {
        void *p;
        //使用malloc开辟空间
        while ((p = malloc(size)) == 0)
        
            if (_callnewh(size) == 0)
            {
                //抛出异常
                static const std::bad_alloc nomem;
                _RAISE(nomem);
        }
    	return (p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    既然operator new 调用的也是malloc,为什么还要封装成 operator new 呢?
    因为C++是面向对象的语言, C是面向过程的语言, 面向过程的语言返回错误是以错误码的形式来返回,面向对象的语言返回错误通过异常和错误来返回, 所以 使用了operator new ,当然 只是原因之一吧,
    operator delete 同理,

    我们是可以手动调用operator new 的
    image.png

    可以看到,不会调用构造函数,也不会调用析构函数

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

    image.png

    可以看到是使用_free_dbg来释放空间的

    #define free§ _free_dbg(p, _NORMAL_BLOCK)

    所以是使用free来释放空间的,

    但是delete 是先调用析构函数, 再去 调用operator delete

    ☝总结

    ⚪️ 对于内置类型

    对于内置类型来说,new delete 与 new[] delete [] 的区别就是new delete 开辟和释放的是单个对象,而
    new[] delete []是多个对象 ,与malloc 和free 的差别并不大, 只不过是简化了使用
    new在申请失败时会直接抛出异常,而malloc返回nullptr

    🔴对于自定义类型
    1. new的原理
      1. 调用operator new 开辟空间
      2. 在申请的空间上调用构造函数
    2. delete的原理
      1. 在空间上执行析构函数,完成对象中资源的清理工作
      2. 调用operator delete函数释放对象的空间
    3. new T[N]的原理
      1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
      2. 在申请的空间上执行N次构造函数
    4. delete[]的原理
      1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
      2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释
        放空间

    ☝placement-new (定位 new)

    定位new : 在已分配的原始内存空间中调用构造函数初始化一个对象

    使用格式:
    new (place_address) type或者new (place_address) type(initializer-list)
    place_address必须是一个指针,initializer-list是类型的初始化列表

    这个一般是配合内存池使用,因为内存池分配出的内存没有初始化,如果是自定义类型的对象,需要调用构造函数

    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{
    		cout << "A()" << endl;
    	}
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    private:
    	int _a;
    };
    
    int main(void)
    {
    	A* pa = (A*)malloc(sizeof(A));
    	//调用定位new,1是初始化列表, 构造函数中的参数
    	new(pa)A(1);
    	
    	//我们可以手动调用析构函数
    	pa->~A();
    	operator delete(pa);
    
    
    	return 0;
    }
    
    
    • 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

    💛常见面试题

    malloc/free和new/delete的区别

    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在释放空间前会调用析构函数完成
      空间中资源的清理

    💛内存泄漏

    什么是内存泄漏:
    内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
    内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
    内存泄漏会导致响应越来越慢,最终卡死。

    C/C++程序中一般我们关心两种方面的内存泄漏:

    • 堆内存泄漏(Heap leak)
      堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这分空间将无法再被使用,就会产生Heap Leak。
    • 系统资源泄漏
      指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
      掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

    内存泄漏会造成非常严重的事故,你想想,服务器程序,一天少一点点,然后程序崩溃宕机, 对于已经上线的项目来说是非常严重的事故
    我们随便写一个代码虽然内存泄漏了,但是不会造成事故的原因是: 进程结束后, 系统会自动回收该进程的空间, 而对于长期运行的程序来说,就会造成很大的错误

    那么如何避免内存泄漏呢 ?

    当然是靠我们自身的设计规范,写代码规范,C++11引入了智能指针,可以极大程度的避免内存泄漏,我们后边再聊

    💛本文结尾

    那么文章就到这里结束了,希望大家在C/C++学习过程中,能更好的使用动态内存分配,有想要交流的小伙伴,可以私信我哦
    image.png

  • 相关阅读:
    图片OCR转表格:终极攻略,优缺点全解析
    Netty 如何做到单机百万并发?
    elementUI使用el-upload上传文件写法总结及避坑,上传图片/视频到本地/服务器以及回显+删除
    【Redis】实战篇:商户查询缓存(缓存穿透、缓存雪崩、缓存击穿问题)
    Linux 安装zsh和zsh的配置
    LabVIEW在无线设备中的应用
    C++ vector
    SAP S4后的一些注意点(一)(更新中)
    【vue-router 路由篇】 传入私有数据给目标路由地址而不显示在url上
    使用Jest搭建自动化单元测试框架为Vue 3项目
  • 原文地址:https://blog.csdn.net/weixin_54580407/article/details/127986970