• C/C++内存管理


    一、C/C++内存分布

    1. 内存分区

    image-20220911121632192

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

    2. 程序运行时加载过程

    首先,我们平时所写的代码(如test.cpp)存放在那?

    其实是存放在磁盘上的,因为是文件形式

    而运行一个程序的过程是怎么样的?

    写好的代码 -> 编译链接 -> 可执行程序

    也就说,我们写好代码点击运行就是运行的这个可执行程序

    可执行程序(Windows下.exe、Linux下a.out)中包括:

    1. 二进制指令代码(CPU读取)
    2. 数据
    3. 其他一些内容

    而当程序运行时首先要加载哪些东西到内存呢?

    1. 二进制指令代码 -> 代码段
    2. 常量数据 -> 代码段
    3. 全局变量 -> 数据段 (因为全局变量在main函数前已经定义好)

    堆栈上的数据何时创建?

    1. 当二进制指令代码加载到代码段之后,由CPU依次读取并执行二进制指令

    2. 当开始执行main函数时,开始创建函数栈帧,此时栈中开辟的变量和数据就开始定义了。

    3. 而堆上的数据也是在栈上,通过malloc等动态开辟内存的函数来开辟的

    注意

    类、函数、符号表、公共代码区等概念都是在编译链接阶段的概念
    
    编译链接之后,类、函数等都转变成了二进制指令加载到代码了,
    
    不存在所谓的类或者函数了
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3. 常见数据类型在内存中的分布

    image-20220911122632879

    二、动态内存管理

    C/C++中,除了堆以外的内存分区中资源的申请与释放不用我们管,系统会自动处理。(所以内存泄漏等问题出现在堆上)

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

    malloc : 申请一块空间

    calloc :申请一块空间并初始化

    realloc :对一段空间进行扩容

    使用完都需要 free,防止内存泄漏

    void Test ()
    {
    	int* p1 = (int*) malloc(sizeof(int));
    	free(p1);
    // 1.malloc/calloc/realloc的区别是什么?
    	int* p2 = (int*)calloc(4, sizeof (int));
    	int* p3 = (int*)realloc(p2, sizeof(int)*10);
        
    // 这里需要free(p2)吗? 
    // 注意: 不需要
        // 因为如果原地扩容:释放p3就相当于释放了p2
        // 如果异地扩容,realloc会自动完成原始空间的free
    	free(p3 );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2. C++动态内存管理

    虽然C++向下兼容C,但是有些地方C的方式是无能为力的,所以C++又搞了一套自己的动态内存管理方式

    即:new 和 delete操作符

    注意:new和delete 不是函数,是操作符

    ① new和delete针对 内置类型

    1. 申请/释放一个int大小的空间

      int* p1 = new int

      delete p1

    2. 申请/释放5个int大小的空间

      int* p2 = new int[5]

      delete[] p2

    3. 申请/释放一个int大小的空间并初始化

      int* p3 = new int(5)

      delete p3

      注意:new int(5) 与 new int[5]的区别!

    int* p1 = (int*)malloc(sizeof(int));
    if(p1==NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    //申请一个int类型大小的空间 
    int* p2 = new int;//new自己去算int是多少字节
     
    //开辟多个int
    int* p3 = new int[5];//开5个int大小的空间
    
    //申请一个int类型大小的空间,并初始化
    int* p4 = new int(5); //申请一个int大小的空间,并初始化为5
    
    //对new的数组初始化 :{} (C++98不支持,C++11才支持)
    int* p5 = new int[5]{ 1,2,3,}; //初始化为 1 2 3 0 0 (不给的默认给0)
    
    
    	//释放
    
    free(p1);//C的释放方式
    
    delete p2; // 针对new int
    delete p4; // 和 new int()
    delete[] p3; //针对new int[] 
    
    • 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

    注意

    new/delete 和 new[] /delete[] 一定要匹配,否则有时候会出现崩溃。一般来说自定义类型一定会报错,自定义类型一般不会报错

    结论

    1. 针对内置类型,new/delete 和 malloc/free没有本质的区别,只有用法上的区别,new/delete只是用法简化了

    2. malloc的需要去检查是否开辟成功,new不需要,如果失败默认会抛异常

    ② new和delete针对自定义类型

    new和delete针对内置类型与C的malloc/free无大区别

    但是对于自定义类型,区别很大!

    这也是引入new/delete的原因

    new/delete针对自定义类型,与malloc/free最大的区别

    1. 就是new的时候会自动调用 默认构造函数(如果无默认构造会报错)

    2. delete的时候会自动调用析构函数

    单个自定义类型对象的构造/析构

    //malloc只是申请空间
    A* p1 = (A*)malloc(sizeof(A));
    if (p1 == nullptr)
    {
    		perror("malloc fail");
    		exit(-1);
    }
    
    //1. new申请空间 2. 调用默认构造函数初始化
    A* p2 = new A;
    A* p3 = new A(10);// 显示传递参数来构造
    	
    free(p1);
    //1. 调用析构函数清理对象中的资源 2. 释放空间
    delete p2;
    delete p3;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    多个对象初始化

    1. 多个对象默认构造初始化
    A* p4 = new A[10];//默认构造(10次)
    delete[] p4;
    
    • 1
    • 2
    1. 多个对象显示构造初始化

      /* 写法比较多 */
      
      A* p5 = new A[10]{ 1,2,3,4,5 };//开10个,前5个显示调用构造初始化,后5个默认构造
      
      A* p6 = new A[]{ 1,2,3,4,5 };//后面有几个数组就开几个对象的大小
      
      A* p7 = new A[]{ (1),(2) };//用括号
      A* p8 = new A[]{ {1},{2} };//用花括号
      
      A* p9 = new A[]{ A(1),A(2)};//用匿名构造
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

      最常用的是第一种和第二种

      注意:支持C++11的编译器才可以显示构造初始化

      VS2013就不支持,2019以上是支持的

    结论

    new/delete是为自定义类型准备的,不仅在堆上申请出来,还会调用构造和析构函数初始化和清理

    因为如果采用malloc申请自定义类型,是无法进行初始化的,因为构造函数是在对象定义的时候自动调用

    malloc只是申请了空间,无法直接调用构造函数进行初始化,也没办法通过访问成员变量进行初始化(因为一般都是私有的)

    ③ malloc与new失败时的区别

    malloc失败时,会返回NULL

    new失败时,会抛异常

    测试代码:

    //malloc失败
    void test3()
    {
    	char* p1 = (char*)malloc(1024u * 1024u * 1024u * 2);//+u是为了防止整形溢出
    	printf("%p\n", p1);//以地址形式打印出p1
    }
    
    //输出结果: 00000000
    // 即:malloc失败,返回NULL
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    new失败

    char* p2 = new char[1024u * 1024u * 1024u * 2 - 1];//-1是因为规定不能超过0x7fffffff即整形的一半
    
    • 1

    运行结果:

    image-20220911192700761

    所以,malloc需要检查返回值看是否malloc失败

    new是不需要检查返回值的,失败会抛异常

    3. operator new和 operator delete函数(底层)

    我们知道,new一个对象其实做了两件事

    1. 申请内存
    2. 调用构造函数

    那么new申请内存是调用了谁来申请内存呢?

    事实上,new和delete是用户进行动态申请内存和释放操作符,operator new 和 operator delete是系统提供的全局函数

    new在底层调用operator new全局函数来申请空间

    如图可见:new操作符被解析为指令的时候,主要是 1. call operator new函数 2. call A::A(构造函数)

    image-20220911200531507

    注意:operator new就是一个函数名,不是运算符重载,只是名字很挫

    ① operator new和operator delete的底层原理

    operator new的源代码

    /*
    operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
    失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
    则抛异常。
    */
    void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
    {
    	// try to allocate size bytes
    	void* p;
        //如果malloc失败,返回NULL 则抛异常
    	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
    • 20

    可以看到,operator new开空间实际上他也是调用了malloc函数来开空间,只是operator new采用了如果失败就抛出bad_alloc的异常的做法。

    这也是为什么要用operator new而不直接采用malloc的原因

    总结operator new的作用

    1. 帮助new开空间
    2. 封装malloc,以复合C++new的失败机制(抛异常)

    operator delete

    /*
    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

    operator delete是调用free来进行释放资源的

    注意:free实际上也是宏定义的,原型是_free_dbg

    free是为了方便用户使用

    /其他的一些加锁和检查可以忽略/

    所以,new/delete对于内置类型其实和malloc/free没有本质区别,因为从底层来看,内置类型不需要调用构造函数和析构函数

    ② 直接使用operator new开辟空间(了解)

    用法和malloc一样,搭配抛异常的try catch使用

    实际上不需要使用这个,直接用new delete即可

    //operator new的使用
    	try
    	{
    		char* ptr = (char*)operator new(1024u * 1024u);//operator new开空间
    		printf("%p\n", ptr);//打印出地址
    		operator delete(ptr);
    	}
    	catch (const std::exception& e)
    	{
    		cout << e.what() << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ③ operator new和operator delete的重载(了解)

    1) 利用重载找出内存泄漏

    有时候我们不想用系统提供的operator new和operator delete

    那么我们就可以进行重载我们自己的operator new和

    operator delete来完成一些特殊的需要

    如:我们想要检测哪里存在内存泄漏,这时候就可以自己重载一个operator new,至于operator delete则自己写一个全局的(不是重载)

    1. 我们在自己重载的operator new中打印文件、函数、行号、申请字节数,然后调用全局的 ::operator new

    2. 自己写一个全局的operator delete,这样delete的时候就会先调用自己写的operator delete了

    注:__FILE__,__FUNCTION____LINE__等是C语言中的宏

    分别是当前文件名当前函数名当前行号

    // 重载operator new,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个字节
    void* operator new(size_t size, const char* fileName, const char* funcName,
    	size_t lineNo)
    {
    	void* p = ::operator new(size);
    	cout <<"new:" << fileName << "-" << funcName << "-" << lineNo << "-" << p << "-"
    		<< size << endl;
    	return p;
    }
    //自己实现全局operator delete(自己实现就不会走库里面的)
    void operator delete(void* p)
    {
    	cout << "delete:" << p << endl;
    	free(p);
    }
    
    // 重载operator delete,这里用不到,只是为了匹配重载的operator new来消除警告
    void operator delete(void* p, const char* fileName, const char* funcName,
    	size_t lineNo)
    {
    	cout << "delete:" << fileName << "-" << funcName << "-" << lineNo << "-" << p <<
    		endl;
    	::operator delete(p);
    }
    
    // 使用条件编译和宏对调用进行简化
    // 只有在Debug方式下,才调用用户重载的 operator new
    //条件编译需要放在函数重载的下面,就把函数名的new替换了 会出错
    #ifdef _DEBUG
    #define new new(__FILE__, __FUNCTION__, __LINE__)
    #endif
    //还需要在main的上面,否则不进行替换程序就走完了
    
    int main()
    {
    	
    	A* p1 = new A;//new的时候 调用重载的operator new
        delete p1;//delete时调用自己写的全局operator delete
        
        A* p2 = new A[4];
        //delete[] p2;
        
        A* p3 = new A;
        delete p3;
        
        A* p4 = new A;
        //delete p4;
        
        A* p5 = new A;
        delete p5;
    	return 0;
    }
    //main函数中 申请5个对象 但是只释放3个  
    // 要找出没有释放的两个!
    
    • 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
    • 50
    • 51
    • 52
    • 53
    • 54

    image-20220912093244145

    程序运行结果如图

    new了5个,但是只delete了3个,并且再通过行号就可以查出具体是哪里没释放而导致的内存泄漏了

    2) 重载一个类专属的operator解决频繁申请空间带来的空间碎片问题(内存池)

    我们知道,如果一个类,比如ListNode类(链表),需要频繁地向内存申请空间

    这样就容易造成一些内存碎片问题。这里就可以利用池化技术来减少内存碎片问题,即内存池

    内存池的工作原理:

    因为malloc是去向堆申请内存,但是要知道操作系统是很忙的,如果频繁的申请就会经常打断操作系统的资源的分配

    而内存池就相当于中间的一个角色。内存池先申请一部分内存,当你想要开辟空间首先到内存池中开辟,当内存池中的

    内存用完,才会再去向堆上申请。这样就减少了请求操作系统的次数,提高效率。

    用个比喻的话就是,堆是你妈妈的钱包,内存池是你自己的钱包。你的定期生活费就是内存池提前申请的内存

    我们可以再类中重载一个operator new和operator delete函数。(注意不是函数重载,也不是运算符重载,只是命名空间即域不同)在我们重载的函数中使用内存池(可以自己写,也可以调用STL或者Boost库或第三方库中的内存池)

    当new一个对象和delete一个对象的时候,机制决定了会先去类中找有没有类专属的operator new 和 operator delete

    找到了就直接使用,找不到才回去全局找。并且因为是类专属的,其他的类的new/delete并不受影响

    所以会直接调用我们在类中重载的operator new和operator delete函数,也就是先去我们定制的内存池中申请内存而不会直接向堆上申请内存

    这里演示STL中的内存池allocator(空间配置器)

    allocate:申请空间

    deallocate:释放空间

    //重载类专属operator new
    struct ListNode
    {
    	int _val;
    	ListNode* _next;
    	static allocator<ListNode> _alloc;//把内存池设置为静态成员变量(声明)
    	//每个ListNode对象都可以访问到,属于整个类
    	void* operator new(size_t n)
    	{
    		cout << "void* operator new(size_t n) -> STL内存池allocator申请" << endl;
    		//allocator类
    		void* obj = _alloc.allocate(1);
    		return obj;
    	}
    	void operator delete(void* ptr)
    	{
    		cout << "void* operator delete(size_t n) -> STL内存池allocator释放" << endl;
    		_alloc.deallocate((ListNode*)ptr,1);
    	}
    	ListNode(int val)
    		:_val(val)
    		, _next(nullptr)
    	{
    
    	}
    
    };
    //类外面定义内存池
    allocator<ListNode> ListNode::_alloc;
    int main()
    {
        //频繁申请ListNode 想提高效率  申请ListNode时,不去malloc而是走自己定制的内存池
    	ListNode* node1 = new ListNode(1);
    	ListNode* node2 = new ListNode(1);
    	ListNode* node3 = new ListNode(1);
    	delete node1;
    	//delete node2;
    	//这里还可以作内存泄漏的检测 申请和释放的个数不符合就是内存泄漏
    	delete node3;
    	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

    image-20220911222935774

    很容易看出存在一个内存泄漏

    ps:(其实更官方一些的检查内存泄漏的并不是这样一个个数,而是用一个数据结构,在operator new的时候把地址存进来

    operator delete的时候把它删除,最后进行查找,剩下的就是没被释放的)

    4. new/delete的实现原理

    ① 内置类型

    如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:

    new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申

    请空间失败时会抛异常,malloc会返回NULL

    ② 自定义类型
    • 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来释
        放空间

    5. 定位new表达式(Replace new)(了解)

    我们知道,构造函数的调用时机常见的就是:

    1. 直接创建对象的时候,自动调用构造函数初始化
    2. new对象的时候,自动调用构造函数

    但是上面两种方式都是在开空间创建对象的时候调用构造,我们是不可以显示调用构造函数的(可以显示调用析构函数)

    并且也不可以访问成员变量直接初始化

    那么如何对已经分配好空间的对象调用构造函数来初始化呢? 这就是定位new的作用

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

    但是析构的话需要自己显示调用

    使用格式:

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

    // 对malloc或者operator new的空间 用定位new调用构造函数初始化
    int main()
    {
    	//p2现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
    	//	有执行
    	A* p2 = (A*)malloc(sizeof(A));
    	if (p2 == NULL)
    	{
    		perror("malloc fail");
    	}
    	//定位new 初始化已经开辟好的空间
    	//new(p2)A; //不给参数
    	new(p2)A(10);//给参数
    
    	//free p2前需要手动析构,释放资源
    	p2->~A();
        free(p2);//free p2
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    使用场景

    .相比new来说,new即开了空间还自动初始化,为啥还要有定位new呢?

    这里就还是因为存在 内存池 的应用场景

    如果我们去内存池申请空间,那么内存池只是开空间,并不会调用构造函数初始化

    所以我们要对开好的空间进行初始化就必须使用定位new

  • 相关阅读:
    【代码随想录算法训练营】第50天 | 第九章 动态规划(十一)+ 复习第20天 第六章 二叉树(六)
    学习太极创客 — MQTT(四)服务端连接操作
    09 创建型模式-建造者模式
    Spring——【第一章入门】:核心Aop与Ioc
    卡特兰数(高精度乘法压位)
    华为云云耀云服务器L实例评测|教你搭建第一个Java程序
    自动驾驶感知算法实战16——激光雷达点云处理原理与实战
    房产新闻查询易语言代码
    openpnp - 底部相机高级矫正后,底部相机看不清吸嘴的解决方法
    ​LeetCode解法汇总307. 区域和检索 - 数组可修改
  • 原文地址:https://blog.csdn.net/K_04_10/article/details/126812830