C++(第五篇):动态内存管理(C/C++内存分布、new / delete的使用及其底层原理、operator new / operator delete函数、内存泄漏)

📒博客主页:Morning_Yang丶
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文所属专栏: 【C++拒绝从入门到跑路】
✉️坚持和努力从早起开始!
💬参考在线编程网站:🌐牛客网🌐力扣
🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!
在这里插入图片描述

一. C/C++内存分布

image-20220426092633685

【说明】

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

对于这几个区域,有如下特点

比如32位操作系统下,其虚拟内存(进程地址空间)只有4G(注:即使物理内存有8G,也是用不完的):

  1. 堆是很大的,内核空间大概1G,剩余的几个区域共3G,其中大部分都是堆的。
  2. 栈是很小的,Linux下一般只有8M左右。(所以递归调用太深,会导致栈溢出)
  3. 数据区和代码区也不是很大。

👉为了能更好的理解内存分布,我们来看如下问题:

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在哪里?__C__ staticGlobalVar在哪里?_C_
    staticVar在哪里?__C_ localVar在哪里?_D__
    num1 在哪里?__A__
    char2在哪里?__A__ *char2在哪里?_A__	//char2在栈上申请一块空间,并将"abcd"拷贝到这个栈上,*char2找到的是zhan'li
    pChar3在哪里?__A__ *pChar3在哪里?_D__
    ptr1在哪里?_A___ *ptr1在哪里?_B__
    

    分析:

    globalVar全局变量在数据段 staticGlobalVar静态全局变量在静态区

    staticVar静态局部变量在静态区 localVar局部变量在栈区

    num1局部变量在栈区

    char2局部变量在栈区

    char2是一个数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上

    pChar3局部变量在栈区 *pChar3得到的是字符串常量字符在代码段

    ptr1局部变量在栈区 *ptr1得到的是动态申请空间的数据在堆区

  2. 填空题:

    sizeof(num1) = _40_;
    sizeof(char2) = __5__; strlen(char2) = _4__;
    sizeof(pChar3) = __4_; strlen(pChar3) = _4__;
    sizeof(ptr1) = __4_;
    

    分析:

    sizeof(num1) = 40;//数组大小,10个整形数据一共40字节

    sizeof(char2) = 5;//包括\0的空间

    strlen(char2) = 4;//不包括\0的长度

    sizeof(pChar3) = 4;//pChar3为指针

    strlen(pChar3) = 4;//字符串“abcd”的长度,不包括\0的长度

    sizeof(ptr1) = 4;//ptr1是指针

在实际的虚拟空间中,一般 如下图所示进行分配空间.
image-20220804160853630

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

2.1 malloc/calloc/realloc和free

通过 malloc / calloc / realloc & free 库函数进行动态内存管理。

void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
    free(p1);//申请动态开辟空间需要进行释放,否则可能造成内存泄漏
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	
	// 这里需要free(p2)吗?
	free(p3);
    //realloc扩容有两种情形,一是在原空间上进行扩(后面有足够大的空间)
	//二是另找一足够大的空间进行开辟,并将原空间内容拷贝(原空间后没有足够大的空间),且自动释放原空间
    //所以此处只需要释放p3就行了
}
  1. malloc/calloc/realloc的区别是什么?
    malloc在堆上开辟对应字节的空间,不初始化
    calloc开辟对应字节的空间并初始化为0
    realloc调整应经动态开辟空间的大小,可以扩容也可以缩小

堆上最多能开辟的空间在32位的版本下最多可以开辟2G空间

三. C++动态内存管理

3.1 概念

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

思考malloc / free 和 new / delete,有什么区别呢

👉区别1:

  • 如果动态申请的是内置类型,它们没有区别。
  • 如果动态申请的是「自定义类型」,它们有区别。

new/delete操作内置类型:

void Test()
{
	// 动态申请一个int类型的空间
	int* ptr1 = new int;
	// 动态申请一个int类型的空间并初始化为3
	int* ptr2 = new int(3);
	// 动态申请3个int类型的空间
	int* ptr3 = new int[3];
	int* ptr4 = new int[3]{1,2,3};//开辟并初始化
    //释放空间
	delete ptr1;
	delete ptr2;
	delete[] ptr3;
    delete[] ptr4;
}
  • 如图:
    image-20220804161021876

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]delete[]

3.2 new和delete操作自定义类型

用malloc和free时可以看到,构造函数和析构函数不会被调用

class Test
{
public:
	Test()
		: _data(0)
	{
		cout << "Test():" << this << endl;
	}
	~Test()
	{
		cout << "~Test():" << this << endl;
	}
private:
	int _data;
};
void Test2()
{
	// 申请单个Test类型的空间
	Test* p1 = (Test*)malloc(sizeof(Test));
	free(p1);
	// 申请10个Test类型的空间
	Test* p2 = (Test*)malloc(sizeof(Test) * 10);
	free(p2);
}
int	main() 
{
	Test2();
	return 0;
}

image-20220804161236759

而使用new和delete时会发现new会调用构造函数,delete调用析构函数:

class Test
{
public:
	Test()
		: _data(0)
	{
		cout << "Test():" << this << endl;
	}
	~Test()
	{
		cout << "~Test():" << this << endl;
	}
private:
	int _data;
};
void Test3()
{
	// 申请单个Test类型的对象
	Test* p1 = new Test;
	delete p1;
	// 申请10个Test类型的对象
	Test* p2 = new Test[10];
	delete[] p2;
}
int	main() 
{
	Test3();
	return 0;
}

image-20220804161323646

注意

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

四. operator new与operator delete函数

4.1 概念

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

malloc 和 new 出错之后的处理方式不一样(C++ 提供一种新的处理错误的方式:抛异常)

malloc 在申请空间失败时会返回 NULL,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)
  • operator new 函数就是对 malloc 的封装,申请内存失败了,会抛出异常。

    那为什么不直接用 malloc 来申请空间呢?-- 因为 malloc 申请失败了会返回 NULL,就达不到抛异常的机制了,所以产生了 operator new 全局函数。

  • operator delete 函数就是对 free 的封装。

思考:我们可以直接用 operator new 和 operator delete 函数来开辟/释放空间吗?-- 可以的。

operator new 和 operator delete 函数跟 malloc 和 free 函数的使用没啥区别,唯一的区别就是会抛异常。

void Test4()
{
	class A {
	public:
		A() { cout << "A()" << endl; }
		~A() { cout << "~A()" << endl; }

	private:
		int _a = 10;
	};

	A* p1 = (A*)malloc(sizeof(A));
	free(p1);
	
    // 调试发现,operator new和operator delete函数不会调用构造和析构函数
    // 跟malloc和free的唯一区别就是,它会抛异常
	A* p2 = (A*)operator new(sizeof(A));
	operator delete(p2);

	A* p3 = new A;
	delete p3;
}

总结一下

通过上述代码和测试,我们发现,new 和 delete 其实没啥神奇的,就是一层一层的封装。

  • new = operator new( = malloc + 抛异常) + 构造函数
  • delete = 析构函数 + operator delete( = free)

4.2 operator new与operator delete的类专属重载(了解)

概念
对于全局,系统会自动提供operator new/ operator delete。由全局共享,但这样效率不高。所以对于频繁调用的对象进行重载专属的operator new/ operator delete,便于提高效率(池化技术)

示例:链表的节点ListNode通过重载类专属 operator new/ operator delete,实现链表节点使用内存池申请和释放内存

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _data;
	void* operator new(size_t n)
	{
		void* p = nullptr;
		p = allocator<ListNode>().allocate(1);
		cout << "memory pool allocate" << endl;
		return p;
	}
	void operator delete(void* p)
	{
		allocator<ListNode>().deallocate((ListNode*)p, 1);
		cout << "memory pool deallocate" << endl;
	}
};
class List
{
public:
	List()
	{
		_head = new ListNode;
		_head->_next = _head;
		_head->_prev = _head;
	}
	~List()
	{
		ListNode* cur = _head->_next;
		while (cur != _head)
		{
			ListNode* next = cur->_next;
			delete cur;
			cur = next;
		}
		delete _head;
		_head = nullptr;
	}
private:
	ListNode* _head;
};
int main()
{
	List l;
	return 0;
}

五. new和delete的实现原理(🌟)

5.1 内置类型

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

  • new / delete 申请/释放的是单个元素的空间,new[] 和 delete[] 申请/释放的是连续空间
  • new 在申请空间失败时会抛异常,malloc 会返回 NULL。

5.2 自定义类型

new A,做了哪些事情呢?

  1. 先调用 operator new(size) 申请空间(底层其实是调用 malloc 来申请)。
  2. 再调用 A 的构造函数对申请的空间初始化。

delete ptr,做了哪些事情呢?

  1. 先调用析构函数完成 ptr 指向的对象中资源的清理。
  2. 再调用 operator delete(ptr) 函数释放 ptr 所指向的空间(底层其实是调用 free 来释放)。

new A[n],做了哪些事情呢?

  1. 先调用 operator new[](n) 函数申请空间,在 operator new[]() 中实际调用 operator new() 函数来完成 n 个对象空间的申请。
  2. 再调用 n 次 A 的构造函数对申请的空间初始化。

delete[] ptr,做了哪些事情呢?

  1. 先调用 n 次析构函数完成 ptr 指向的 n 个对象中资源的清理。
  2. 再调用 operator delete[](ptr) 函数释放 ptr 指向的空间,在 operator delete[]() 中实际调用 operator delete() 函数来释放空间。

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

👉思考:构造函数在定义对象时自动调用的,但现在我这里有一块已分配的空间,想要调用构造函数对这块空间初始化,有没有什么办法呢?

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

使用格式

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

  • 例:
class A {
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }

private:
	int _a = 10;
};

int main()
{
	// 比如我用malloc申请了一块空间,想要显式调用构造函数对其初始化
    
    // p现在指向的只不过是与A对象相同大小的一块空间,还不能算是一个对象,因为构造函数没有执行
	A* p = (A*)malloc(sizeof(A));

	// 定位new表达式:显式的对一块空间调用构造函数初始化
	new(p) A();  // 注意:如果A类的构造函数有参数时,此处需要传参

	// 析构函数可以直接显式调用
	p->~A();
	free(p);

	return 0;
}

使用场景

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

  2. 复制一份 a 数组到另外一块空间中去。

    传统做法:

    int main()
    {
    	A a[5];
        
        // 调用5次构造函数,再调用5次拷贝构造函数 
    	A* p = new A[5];
    	for (int i = 0; i < 5; i++)
    	{
    		p[i] = a[i];
    	}
    	return 0;
    }
    

    使用定位 new 表达式做法:

    int main()
    {
    	A a[5];
    
    	A* p = (A*)malloc(sizeof(A) * 5);
    	for (int i = 0; i < 5; i++)
    	{
    		new(p + i) A(a[i]); // 直接调用5次拷贝构造就行了
    	}
    
    	return 0;
    }
    

七. 常见面试题

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

7.2 内存泄漏(🌟)

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

危害

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

① 内存泄漏分类

  1. 堆内存泄漏(Heap leak):
void MemoryLeaks()
{
    // 1.内存申请了忘记释放
    int* p1 = (int*)malloc(sizeof(int));
    int* p2 = new int;
    // 2.异常安全问题
    int* p3 = new int[10];
    Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
    delete[] p3;
}

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

  1. 系统资源泄漏:

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

② 如何避免内存泄漏:

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

③ 如何一次在堆上申请4G的内存

因为 32 位的进程,虚拟内存(进程地址空间)只有4G,不可能申请成功。

而 64 位的进程,虚拟内存(进程地址空间)有$ 2^{64}$ G,所以可以申请成功。

#include <iostream>
using namespace std;
// 将程序编译成 x64 的进程
int main()
{
	try
	{
		void* p = new char[0xfffffffful];
		cout << "new:" << p << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}


八、练习题

1、下面有关c++内存分配堆栈说法错误的是( )

A.对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制

B. 对于栈来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆来讲,它的生长方向是向上的,是向着内存地址增加的方向增长

C.对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题

D.一般来讲在 32 位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的

答案解析

A.栈区主要存在局部变量和函数参数,其空间的管理由编译器自动完成,无需手动控制,堆区是自己申请的空间,在不需 要时需要手动释放

B.栈区先定义的变量放到栈底,地址高,后定义的变量放到栈顶,地址低,因此是向下生长的,堆区则相反

C.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

D.32位系统下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用,故错误


2、C++中关于堆和栈的说法,哪个是错误的:( )

A.堆的大小仅受操作系统的限制,栈的大小一般较小

B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题

C.堆和栈都可以静态分配

D.堆和栈都可以动态分配

答案解析

A.堆大小受限于操作系统,而栈空间一般有系统直接分配

B.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

C.堆无法静态分配,只能动态分配

D.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放


ClassA *pclassa=new ClassA[5];
delete pclassa;

3、c++语言中,类ClassA的构造函数和析构函数的执行次数分别为( )

A.5,1

B.1,1

C.5,5

D.程序可能崩溃

答案解析

A.申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃

B.构造函数会调用5次

C.析构函数此时只会调用1次,要想完整释放数组空间,需要使用[]

D.正确

拓展

如果用new 类[]申请空间,用delete释放:

  • 有析构函数,报错
  • 没有析构函数,不报错

VS下,new 类[]的时候会在前面多申请一个int的大小用来保存要调用多少次析构函数,所以正常调用 delete []的时候指针会往前偏移4个字节开始析构。如果类有析构函数,delete不往前偏移,就会报错。如果类没有析构函数,编译器认为类没有资源要释放,调不调用析构函数都无所谓,new 类[]的时候就不会在前面多申请一个int的大小用来保存要调用多少次析构函数,所以delete的时候不偏移也不报错,刚好在申请空间的起始位置。(不同编译器下处理方式是不同的)


4、使用 char* p = new char[100]申请一段内存,然后使用delete p释放,有什么问题?( )

A.会有内存泄露

B.不会有内存泄露,但不建议用

C.编译就会报错,必须使用delete []p

D.编译没问题,运行会直接崩溃

答案解析

A.对于内置类型,此时delete就相当于free,因此不会造成内存泄漏

B.正确

C.编译不会报错,建议针对数组释放使用delete[],如果是自定义类型,不使用方括号就会运行时错误

D.对于内置类型,程序不会崩溃,但不建议这样使用


5、下面有关malloc和new,说法错误的是? ( )

A.new 是创建一个对象(先分配空间,再调构造函数初始化), malloc分配的是一块内存

B.new 初始化对象,调用对象的构造函数,对应的delete调用相应的析构函数,malloc仅仅分配内存,free仅仅回收内存

C.new和malloc都是保留字,不需要头文件支持

D.new和malloc都可用于申请动态内存,new是一个操作符,malloc是是一个函数

答案解析

A.new会申请空间,同时调用构造函数初始化对象,malloc只做一件事就是申请空间

B.new/delete与malloc/free最大区别就在于是否会调用构造函数与析构函数

C.需要头文件malloc.h,只是平时这个头文件已经被其他头文件所包含了,用的时候很少单独引入,故错误

D.new是操作符,malloc是函数


6、设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为? ( )

C c;
void main()
{
    A*pa=new A();
    B b;
    static D d;
    delete pa;
}

A. A B C D

B. A B D C

C. A C D B

D. A C B D

答案解析

分析:首先手动释放pa, 所以会先调用A的析构函数,其次C B D的构造顺序为 C D B,因为先构造全局对象,在构造局部静态对象,最后才构造普通对象,然而析构对象的顺序是完全按照构造的相反顺序进行的,所以答案为 B

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Morning_Yang丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值