C语言的动态内存管理你了解吗

 更新时间:2022年03月29日 10:43:44   作者:和太阳肩并肩的老杨  
这篇文章主要为大家详细介绍了C语言的动态内存管理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助

C/C++内存分配方式

在学习C语言阶段的时候,创建一个变量,编译器会为它分配一块内存。而创建一个C++对象的时候,编译器会为这个对象分配内存,并且调用合适的构造函数进行初始化。

那么编译器的内存分配方式是怎样的呢?

内存分配可以有以下的几种方式

  • 从静态存储区分配。这样的分配方式在程序开始前就可以为对象/变量分配,这块空间在整个程序运行期间都存在。
  • 从栈区分配。调用函数时,函数的参数,局部变量,返回地址等存储在堆栈上,函数执行结束时将会自动释放这些内存空间,栈区的内存空间远远小于堆区。
  • 从堆区分配。这种内存分配方式被称为动态内存分配,堆区又被称为“自由存储单元”,运行时通过调用相应的函数来申请和释放内存。

有时候我们并不知道程序中的对象确切地需要多少内存空间,动态内存分配则很好地处理了这种需求。

C++内存管理方式

C库中提供了函数malloc,以及它的变种函数realloc、calloc来动态地申请内存空间。使用函数free来释放动态申请出的内存空间。

int* ptr1 = (int*)malloc(sizeof(int));

使用malloc需要指定空间大小,并且要强制类型转化,因为它只是简单地分配了一块空间,返回的是void*,而C++中不允许将空类型的指针赋予给其他类型的指针。另外,如果你申请一块内存之后,没有对这个指针进行正确的初始化,有可能会导致程序运行失败,并且如果忘记释放动态申请的内存空间,则会造成内存泄露等危害……

在创建一个C++对象时,编译器会做这两件事:

1.为对象分配内存。

2.编译器自动调用构造函数初始化该内存。

构造函数不支持显式地调用,意味着如果使用malloc函数创建一个对象,那么这个对象将不能够调用构造函数,仅仅只是开辟了一块空间。但是我们必须要确保对象被初始化,因为未初始化对象是大部分程序出错的主要原因。总而言之,C中的动态内存管理无法满足C++中动态对象的需求。

所以提出了newdelete.关键字

new和delete的使用

int main(void)
{
	//基本内置类型
	//开辟一个int类型的空间
	int* ptr2 = new int;

	//开辟多个int类型空间
	int* ptr3 = new int[5];

	//开辟一个int类型并初始化为1
	int* ptr4 = new int(1);

	delete ptr2;
	delete[] ptr3;
	delete ptr4;
	return 0;
}

new和delete的使用方式:

1.开辟一个空间: new 类型;       对应释放: delete 对象;2.开辟多个空间:new 类型[个数]   对应释放:delete[] 对象;3.开辟并初始化: new 类型(初始化数据) 对应释放:delete 对象  1.开辟一个空间: new 类型;       对应释放: delete 对象;
2.开辟多个空间:new 类型[个数]   对应释放:delete[] 对象;
3.开辟并初始化: new 类型(初始化数据) 对应释放:delete 对象
  

new和delete的骚操作

内置类型

对于内置类型,new和delete与C的内存管理函数做了差不多的事情,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

自定义类型

new表达式:

  • 调用opreator new函数分配内存
  • 调用构造函数初始化该内存

delete表达式:

  • 调用析构函数清理对象中的资源
  • 调用operator delete释放空间。

operator new( ) 和operator delete( )这两个内存分配函数是系统提供的全局函数,实际上是对malloc和free的各种行为进行了封装。

new和delete的区别

new、delete 和 malloc、free的区别有哪些呢?

  • new和delete是关键字,malloc和free是函数
  • malloc申请的空间不会初始化,new会初始化。
  • malloc需要手动计算空间大小并传递,new不需要
  • malloc的返回值是void*,使用时必须强制类型转换;new不需要,后面跟的是空间的类型。
  • malloc申请失败,返回NULL,所以调用后要判断是否开辟成功;new需要捕获异常
  • malloc只是开辟空间,不会调用构造函数,free释放空间不会调用析构函数;new在申请空间后会调用构造函数初始化对允许象,delete会调用析构函数清理对象中的资源,然后再释放空间。

重载new和delete

C++允许重载new和delete,以实现我们自己的存储分配方案。但是注意重载operator new和operator delete时,仅仅只能改变原本的内存分配方式。同重载其他的运算符一样,可以分为重载成全局和针对特定类的内存分配函数。

重载全局

重载一个全局的new和delete会导致默认版本完全不能被访问。

重载operator new的要求:

  • 必须有一个size_t参数,该参数将接收要开辟空间的长度。
  • 返回一个指向对象的指针,该对象的长度等于或者大于所申请的长度。
  • 如果分配失败,不仅仅要返回一个0,还需产生一个异常信息之类的现象,明确分配内存时出了问题。
  • 返回值是一个void*

重载operator delete的要求

  • 参数是一个指向由operator new()分配的void*类型的内存的指针
  • 返回值是void

为什么重载operator delete的时候,参数是一个void*?

这是因为它是在调用析构函数后得到的指针。

	//重载operator new
	//1.必须有一个size_t的参数,该参数将接收要申请开辟空间的大小
	//2.返回值是一个void*
	//3.返回一个指向对象的指针,该对象的长度等于或大于所申请的长度
	//4.如果分配失败。不仅仅要返回一个0,还需产生一个异常信息
	void* operator new(size_t sz)
	{
		cout << "new %d Bytes" << sz << endl;
		void* p = nullptr;
		//不需要强制类型转换,因为malloc返回的就是void*
		p = malloc(sz);
		if (nullptr == p)
		{
			cout << "new fail\n" << endl;
		}
		return p;
	}
	//重载operator delete
	//1.返回值为void
	//2.参数是一个指向由operator new()返回的void*的指针
	void operator delete(void* rp)
	{
		cout << "operator delete" << endl;
		free(rp);
	}

重载类专属

//重载ListNode专属的operator new
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表达式

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

使用的格式:

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

//例如
class Test
{
public:
	Test()
		: _data(0)
	{
		cout << "Test():" << this << endl;
	}
	~Test()
	{
		cout << "~Test():" << this << endl;
	}

private:
	int _data;
};

int main(void)
{
	// pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	Test* pt = (Test*)malloc(sizeof(Test));

	//定位new
	new(pt) Test; // 注意:如果Test类的构造函数有参数时,此处需要传参
	return 0;
}

内存泄露

内存泄露的含义:

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

内存泄露的两大分类:

1.堆内存泄露(Heap leak)

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

2.系统资源泄露

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

内存泄露的危害:

在平时写一些小测试的时候,并没有觉得内存泄露的危害特别大,但是在长期运行的程序中出现内存泄漏,影响非常的大,出现内存泄露可能会导致响应越来越慢,最终出现卡死的现象。

内存泄露的解决方案分两种:

1.事先预防 。

2. 事后查错

如何事先预防?

1.养成良好的编码习惯,申请了内存要记得释放。

2.采用RAII思想或者智能指针来管理资源。

3.规范使用内部实现的私有内存管理库。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

相关文章

最新评论