• C++ 内存管理


    作者:@小萌新
    专栏:@C++初阶
    作者简介:大二学生 希望能和大家一起进步!
    本篇博客介绍:会为大家系统的梳理一遍C++内存管理的相关知识
    在这里插入图片描述

    本章目标

    1. 熟悉C/C++中的内存分布
    2. 熟悉C语言中的动态内存管理
    3. 理解并熟悉C++中的动态内存管理
    4. 了解operator new与operator delete函数
    5. 了解new和delete的实现原理
    6. 了解定位new表达式(placement-new)
    7. 掌握常见面试题

    一. 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);
    }
    
    1. 选择题:
    选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
    globalVar在哪里?____  staticGlobalVar在哪里?____
    staticVar在哪里?____  localVar在哪里?____
    num1 在哪里?____
    char2在哪里?____  *char2在哪里?___
    pChar3在哪里?____    *pChar3在哪里?____
    ptr1在哪里?____     *ptr1在哪里?____
    2. 填空题:
    sizeof(num1) = ____; 
    sizeof(char2) = ____;    strlen(char2) = ____;
    sizeof(pChar3) = ____;   strlen(pChar3) = ____;
    sizeof(ptr1) = ____;
    
    
    • 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

    其中前面1~5没有什么难点

    全局变量在静态区

    被Static修饰的变量在静态区

    局部变量放到栈区

    故答案为 C C C A A

    我们再来看看后面六个

    指针是在栈区(局部变量)

    动态开辟出的空间是在堆区

    只要记住这两点上面的题目基本不会错

    唯一值得注意的是这一行代码

    char char2[] = "abcd";
    
    • 1

    *char2的在哪里?

    因为这时候的char2是一个指向首元素的地址 (不懂的可以去复习下C语言数组章节)

    对于它解引用出来的是首元素 也就是字符‘a’ 和常量的字符串类型不匹配

    于是这个时候就会在栈上拷贝一份临时的数据

    所以说它在栈区中

    故答案为

    A A A A D A B

    在这里插入图片描述

    看看这张图 之后我们便开始做填空题

    sizeof(num1) = ____; 
    sizeof(char2) = ____;    strlen(char2) = ____;
    sizeof(pChar3) = ____;   strlen(pChar3) = ____;
    sizeof(ptr1) = ____;
    
    • 1
    • 2
    • 3
    • 4

    这里唯一需要注意的一点是 字符串后面的‘/0’ 在计算大小是算作是一个大小 在计算长度时忽略不计

    所以说sizeof(char2) = 5

    其他的题目都很简单 这里直接给出答案

    40 5 4 4 4 4 (32位系统)

    这里提出一个问题 sizeof和strlen的区别是什么?

    答:一个是计算大小占多少个字节 一个是计算字符串长度是多少

    二. C语言中的动态内存管理方式

    C语言中实现内存管理主要是通过下面这四个函数分别是

    malloc

    calloc

    realloc

    free

    其中常见的面试题有

    malloc calloc realloc之间的区别是什么 ?

    这里可以直接参考萌新以前写的博客

    C语言动态内存开辟

    着重要注意下malloc和calloc的区别

    一个是不初始化 一个是全部初始化为0

    三. C++中动态内存管理方式

    我们说C++是基于C语言的优化 所以说C语言中的动态内存管理方式在C++中也是可以使用的

    但是呢在某些特殊场景下 C语言中的动态内存管理会不能操作 或者说操作起来很麻烦

    所以说这个时候我们C++就诞生了新的内存管理方式

    3.1 new和delete操作符操作内置类型

    注意我的副标题写的是什么? 操作符

    说明我们这里的两个关键字是操作符 而C语言中的malloc ralloc cealloc free都是函数 这里要注意

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

    在这里插入图片描述
    我们来看看debug过程

    在这里插入图片描述
    从下面的监视窗口同学们应该就能理解了初始化和new的基本使用了

    我们这里继续走下去

    在这里插入图片描述
    delete之后发现全部被删除了

    这里我们总结下

    总结

    对于申请和释放单个元素的空间 我们使用new和delete
    如果想要初始化 我们在后面加上括号 括号里面写上我们要初始化的值就好
    对于申请和释放连续的空间的时候我们使用 new[] 和delete[]

    这里要注意的一点是 在C++11更新的标准中增加了对于连续空间的初始化方式

    代码表示如下

    int* ptr6 = new int[3]{1,2,3};
    
    • 1

    在这里插入图片描述

    在C++11标准以后 我们也可以使用大括号对于连续的空间进行初始化

    3.2 new和delete操作符操作自定义类型 (重点之一)

    还是一样 我们先来看代码

    class A
    {
    public:
    	A(int a = 0)
    		: _a(a)
    	{
    		cout << "A():" << this << endl;
    	}
    	~A()
    	{
    		cout << "~A():" << this << endl;
    	}
    private:
    	int _a;
    };
    
    struct ListNode
    {
    	ListNode* _next;
    	int _val;
    
    	ListNode(int val)
    		:_next(nullptr)
    		,_val(val)
    	{}
    };
    
    int main()
    {
    	//自定义类型
    	//new和delete相比malloc/free,除了空间管理还会调用构造函数和析构函数
    	A* p1 = new A;
    	A* p2 = (A*)malloc(sizeof(A));
    
    	delete p1;
    	free(p2);
    
    	ListNode* n1 = new ListNode(1);
    	ListNode* n2 = new ListNode(2);
    	ListNode* n3 = new ListNode(3);
    	ListNode* n4 = new ListNode(4);
    	
    	n1->_next = n2;
    
    
    	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
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    在这里插入图片描述
    我们这里发现会自动调用构造函数和析构函数

    这就是为什么C++中会使用new和delete操作符的原因之一

    之后我们再来看看malloc出来的空间 能不能调用构造和析构
    (其实这里已经能猜出来了 不能 )

    在这里插入图片描述
    这里什么都没有发生 符合预期

    在这里插入图片描述
    我们发现对于Listnode来说 也是new出来就初始化了

    比起malloc来说方便不少

    3.3 面向过程和面向对象的错误区别(重点之一)

    面向过程的语言的错误是通过错误码识别的
    面向对象的语言的错误是通过抛出异常识别的

    还记得C语言中我们malloc失败是怎么报错的嘛?

    代码表示如下

    	A* p2 = (A*)malloc(sizeof(A));
    	if (p2==NULL)
    	{
    		perror("malloc fail");
    		exit(-1);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    但是在C++中我们的报错是这样子的

    (这里的语法记住就好 不要求现在掌握)

    int main()
    {
    	try
    	{
    		while (1)
    		{
    			new char[1024u * 1024u * 1024u];
    			cout << "yes" << endl;
    		}
    		cout << "test" << endl;
    	}
    	catch (exception& e)
    	{
    		cout << e.what() << endl;
    	}
    
    	return 0;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    这里我们可以发现两个点

    1 new空间失败之后会直接抛出一个异常到catch 捕获异常
    2 new空间失败后后面的语句不会执行

    为了验证第二点我们可以这样试试

    在这里插入图片描述
    可以完美验证我们的思路

    3.4 类型不匹配遇到的错误

    如果我们使用了new 我们销毁要使用delete
    如果我们使用了new[] 我们销毁要使用delete[]
    如果我们使用了 malloc realloc calloc 我们要使用free

    不能乱

    横穿马路有风险 你这次侥幸没被创不代表你永远不会被创

    四. operator new和operator delete函数

    new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是
    系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过
    operator delete全局函数来释放空间。

    其实底层就是用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);
    }
    /*
    operator delete: 该函数最终是通过free来释放空间的
    */
    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的实现
    */
    #define  free(p)        _free_dbg(p, _NORMAL_BLOCK)
    
    
    • 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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    五. new和delete的实现原理

    5.1 内置类型

    在实现内置类型开辟销毁空间的时候基本和malloc free相似

    唯一不同的是如果开辟失败

    new会抛出一个异常

    malloc会返回一个空指针

    5.2 自定义类型

    new的原理

    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的构造

    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来释
      放空间

    总结下

    new是先开辟空间再调用构造函数

    delete是先调用析构函数再销毁空间

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

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

    它的使用格式如下

    new (place_address) type(无参)
    
    • 1

    或者说

    new (place_address) type(initializer-list)(有参)
    
    • 1

    我们来看看代码

    class A
    {
    public:
    	A(int a = 0)
    		: _a(a)
    	{
    		cout << "A():" << this << endl;
    	}
    	~A()
    	{
    		cout << "~A():" << this << endl;
    	}
    
    	private:
    	int _a;
    };
    
    
    
    int main()
    {
    		A* p1 = new A;
    
    		A* p3 = (A*)malloc(sizeof(A));
    		if (p3 == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    		//new(p3)A(1);
    		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
    • 31
    • 32

    来看看debug窗口是什么样子的

    在这里插入图片描述
    这里的p1初始化了 但是p3没有初始化

    但是我们应该怎么调用构造函数呢?

    这里给出一个叫做定位new的解决方案

    new(p3)A(1);
    
    • 1

    在这里插入图片描述
    我们发现 这里确实初始化成功了

    而析构函数的调用就简单多了 直接指针指向析构函数就可以

    在这里插入图片描述

    七. 常见面试题

    7.1 malloc和new的区别 delete和free的区别

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

    1. malloc和free是函数,new和delete是操作符

    这个我们再前面就讲解过了

    1. malloc申请的空间不会初始化,new可以初始化

    这里就是为什么C++中要创造new 两大创造new的原因之一

    1. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,
      如果是多个对象,[]中指定对象个数即可

    这个是语法上的特点

    1. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

    也是语法上的特点

    1. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需
      要捕获异常

    这个应该是要记住的 两大创造new的原因之一

    1. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new
      在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成
      空间中资源的清理

    这个是malloc和delete的自定义类型实现原理 我们在上面讲解过了

    应该怎么理解并记忆呢?

    首先是语法上的三个特点

    int*p = (int *)malloc(sizeof(4));
    
    • 1
    int* p = new int3;
    
    • 1

    观察这两段代码我们不难发现 2 4

    之后如果开辟多个空间的话 我们不难发现 3

    int* p = new int[5];
    
    • 1

    回答上来这三点之后我们就开始想 为什么C++要创建new

    一个是初始化 一个是抛出异常

    最后我们回顾下new实现自定义类型的原理就好

    7.2 内存泄漏

    什么是内存泄漏呢?

    内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
    该段内存的控制,因而造成了内存的浪费。

    内存泄漏的危害是什么呢?

    长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
    内存泄漏会导致响应越来越慢,最终卡死。

    大家可以写出这段代码 并且打开资源管理器

    我们可以发现 这个程序占用的内存越来越高了

    int main()
    {
    	while (1)
    	{
    		int* p = new int[1024];
    		cout << p << endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    关于怎么预防内存泄漏其实都是我们后面要学的内容

    老师上课的时候也没有重点讲解 所以说博主这里就不提及了 后面遇到再说

    总结

    在这里插入图片描述

    本篇博客博主主要讲解了C++中的内存管理
    由于博主水平有限 所以错误在所难免 希望大佬看到之后可以指正
    如果这篇博客帮助到了你 别忘了一键三连啊
    阿尼亚 哇酷哇酷!

  • 相关阅读:
    Docker从入门到进阶之进阶操作(3) —— 使用 supermin5来构建镜像
    【wiki知识库】05.分类管理模块--后端SpringBoot模块
    Docker搭建私有镜像仓库及推送、拉取私服镜像
    【深入设计模式】迭代器模式模式—什么是迭代器模式?
    【MySQL】MySQL索引的定义、分类、Explain、索引失效和优化
    python爬虫开源项目代码个性化电影推荐系统算法
    [黑马程序员Pandas教程]——DataFrame查询数据
    docker安装elasticsearch7.8和kibana7.8
    快速入门 git 代码版本管理工具(04)
    模拟前端ADC芯片LH001-91
  • 原文地址:https://blog.csdn.net/meihaoshy/article/details/127733837