• 1024程序员节特辑 | 深度解析C/C++内存管理(建议收藏!!)




    一、C/C++内存分布

    C/C++内存分布大致如下:

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

    1.1 相关例题

    代码如下:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    分析图:
    在这里插入图片描述
    问题:

    1. 选择题:
      选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
      globalVar在哪里?____ staticGlobalVar在哪里?____
      staticVar在哪里?____ localVar在哪里?____
      num1 在哪里?____
      char2在哪里?____ *char2在哪里?___
      pChar3在哪里?____ *pChar3在哪里?____
      ptr1在哪里?____ *ptr1在哪里?____
      答案:C、C、C、A、A ;
      A、A、A、D、A、B
    1. sizeof 和 strlen 区别?
      sizeof是一个运算符,用于获取对象或类型的大小(以字节为单位)。它可以用于任何类型的变量,包括字符串。
      strlen是一个函数,用于获取以null字符(‘\0’)结尾的字符串的长度(以字符为单位)。它只能用于字符串,不能用于其他类型的变量。

    二、 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)吗?
    	free(p3 );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.1 相关面试题

    【面试题】:

    1. malloc/calloc/realloc的区别?
      解析:malloc用于分配指定大小的内存块;calloc用于分配指定数量和大小的连续内存块并初始化为0;realloc用于重新分配已经分配的内存块的大小。

    2.malloc的实现原理?
    解析:glibc中malloc实现原理


    三、C++内存管理方式

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

    3.1 new/delete操作内置类型

    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
    • 13
    • 14

    图解:
    在这里插入图片描述
    小tips:

    1. 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。
    2. 相对于malloc,new不需要强转,并且会自动计算大小,。除此之外,还额外支持初始化!!

    3.2 new和delete操作自定义类型

    下面给出new和delete自定义类型的几种常见用法:

    class A
    {
    public:
    	A(int a = 0)
    		: _a(a)
    	{
    		cout << "A():" << this << endl;
    	}
    
    	~A()
    	{
    		cout << "~A():" << this << endl;
    	}
    
    private:
    	int _a;
    };
    
    int main()
    {
    	//malloc没有办法很好的支持自定义类型动态申请空间的初始化-->new
    	A* p1 = (A*)malloc(sizeof(A));
    	//p1->A(1); 创建对象时,自动调用,不可显示调用
    
    	//new = malloc + 构造函数 (申请空间+ 初始化)
    	A* p2 = new A;
    	A* p3 = new A(1);
    	//delete = 析构函数 + free
    	delete p2;
    	delete p3;
    
    	A* p4 = new A[10];
    	delete[] p4;
    
    	A a1(1);
    	A a2(2);
    	A* p5 = new A[10]{ a1,a2 };
    	delete[] p5;
    
    	//匿名对象
    	A* p6 = new A[10]{ A(1), A(2) };
    	delete[] p6;
    
    	//隐式类型转换
    	A* p7 = new A[10]{ 1,2 };
    	delete[] p7;
    	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

    Tips:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。


    四、operator new与operator delete函数

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

    operator new与operator delete本质上是malloc和free的封装,只不过operator new申请空间失败不会返回空指针,而是抛异常。

    具体封装如下:

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

    通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

    4.1 operator new抛异常演示

    下面通过开足够大的空间,同时通过try…catch来捕捉异常

    void func()
    {
    	char* ptr = new char[0x7fffffff];
    	cout << (void*)ptr << endl;
    }
    
    int main()
    {
    	try
    	{
    		//char* ptr = new char[0x7fffffff];
    		//*cout << ptr << endl;*//对于char*, cout不会按指针打印,会按照字符串打印。
    								//同时由于编码等问题,会报屯屯...
    		//cout << (void*)ptr << endl;
    
    		func();
    		cout << "hello C++" << endl;
    	}
    	catch (const 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
    • 21
    • 22
    • 23
    • 24
    • 25

    捕捉异样结果:
    在这里插入图片描述


    五、new和delete的实现原理

    5.1 内置类型

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

    5.2 自定义类型

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

    5.3 相关例题

    class A
    {
    public:
    	A(int a = 1)
    		:_a(a)
    	{
    	
    	}
    	~A()
    	{
    		cout<<"~A"<<endl;
    	}
    
    private:
    	int _a;
    };
    int main()
    {
    	A* p2 = new A[10];
    	delete p2;
    	return 0;
    }
    int main()
    {
    	//Stack* ptr = (Stack*)operator new(sizeof(Stack));
    	//operator delete(ptr);
    
    	A* p2 = new A[10];
    	//delete[] p2;
    	//free(p2);
    	delete p2;
    	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

    Tips:没有显示的写析构函数是成员正常运行,显示写后会报错。
    解析:


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

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

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

    使用场景:
    定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如
    果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

    class A
    {
    public:
    	 A(int a = 0)
    	 : _a(a)
    	 {
    		 cout << "A():" << this << endl;
    	 }
    	 ~A()
    	 {
    		 cout << "~A():" << this << endl;
    	 }
    private:
    	 int _a;
    };
    
    // 定位new/replacement new
    int main()
    {
    	 // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    	 A* p1 = (A*)malloc(sizeof(A));
    	 new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
    	 p1->~A();
    	 free(p1);
    	 
    	 A* p2 = (A*)operator new(sizeof(A));
    	 new(p2)A(10);
    	 p2->~A();
    	 operator delete(p2);
     	 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

    七、7. 常见面试题

    7.1 malloc/free和new/delete的区别


    malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
    5. . malloc和free是函数,new和delete是操作符。
    6. malloc申请的空间不会初始化;new可以初始化。
    7. malloc申请空间时,需要手动计算空间大小并传递;new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
    8. malloc的返回值为void*, 在使用时必须强转;new不需要,因为new后跟的是空间的类型。
    9. malloc申请空间失败时,返回的是NULL,因此使用时必须判空;new不需要,但是new需要捕获异常。
    10. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

    7.2 内存泄漏

    7.2.1 什么是内存泄漏,内存泄漏的危害


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

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

    7.2.2 内存泄漏分类

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

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

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

    7.2.3 如何检测内存泄漏

    在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks()函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息

    int main()
    {
    	 int* p = new int[10];
    	 // 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
    	 _CrtDumpMemoryLeaks();
    	 return 0;
    }
    
    	// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
    	Detected memory leaks!
    	Dumping objects ->
    	{79} normal block at 0x00EC5FB8, 40 bytes long.
    	Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
    	Object dump complete.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的。

    7.2.4如何避免内存泄漏

    1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
    2. 采用RAII思想或者智能指针来管理资源。
    3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
    4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

    总结一下:
    内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

  • 相关阅读:
    如何获取论文资源?
    华为智能高校出口安全解决方案(3)
    索引设计的原则?
    突破界限的力量:探索Facebook如何打破国界、文化和语言的障碍
    axure入门
    C++ 跨平台UI框架 JUCE
    信息学奥赛中的STL(标准模板库)--2022.09.30
    React-Redux
    SLAM从入门到精通(构建自己的slam包)
    Nacos报错 Can‘t find dependent libraries(坑!)
  • 原文地址:https://blog.csdn.net/Zhenyu_Coder/article/details/133956448