内存分配方式介绍-面试最喜欢问的东西

前言

关于内存分配方式,这可是面试官最喜欢问的问题了。前几天笔试一家公司的我也被问到了这样的问题。难么?朋友,肯定不难,咱们今天就把malloc,free,new,delete以及内存分配方式都讲清楚(我尽力吧)

跑偏了

看了别人的回答我才发现,原来内存分配表示的是静态存储区域、栈和堆。。。。。。我丢,看来是真的跑偏了。没事儿,咱们重温一下这仨东西。

静态存储区

比如一些全局变量,或者是static静态变量,在程序编译的时候就已经确定下来了,然后也开辟了内存给他们用,程序结束后系统帮我们释放。初始化的全局or静态变量放在一起,未初始化的全局和静态放在另一块地方(BSS段了解一下?)

栈区

比如函数中定义的局部变量,函数的参数值,都可以是在栈中。栈大小一般比较小,当然VS里可以设置,将原本的1M或者2M设置成更大。

堆区

就很自由了,也叫动态内存区。咱们自己开辟内存,管理内存,释放内存。如果咱们自己申请了内存但是没释放,可能造成内存泄漏。另外如果堆内存管理的不好,可能造成内存碎片。

我当时是怎么回答的

僵硬了,我当时没想到居然是这么个简单的回答,我回答成,堆区的内存管理方式了(内存堆和内存池)。那也没关系,咱们这里就将内存管理的方式给大家介绍一下。

传统的malloc free / new delete方式

先给各位扯明白这两组的区别:
1.malloc free是库函数,new delete是关键字
2.malloc free需要指定分配大小,new delete根据分配类型确定
3.malloc分配失败返回NULL,new分配失败返回bad_alloc
4.new不仅申请内存,还执行对象的构造(delete是析构)函数
5.存放的地方不同。malloc是在堆上,new是在free store上(堆是操作系统级别的东西,free store是语言级别的),如果我开发c++,那么我可以将free store设置成堆,我当然也可以设置成静态存储区。(当然咱们现在是new在堆上,所以存放地址没啥区别)

咱们一起看看,malloc是如何实现堆内存管理的。
其实说白了,就是在一段连续的内存地址上,将已经分配的和没有分配的内存都用一个mem_ctrl_block这样的struct来管理。咱们现在自己来实现一个malloc,叫myMalloc,开始:

struct mem_heap_head
{
	bool IsFreeData;
	long MemHeapDataLength;
};

void* memory_heap_start;
void* memory_heap_end;

下面是一些基本的操作:

//private:将数据部分地址转换成对应的控制字首部指针
void* ToHeadPtr(void* addr)
{
	if (addr == nullptr)
		return nullptr;
	struct mem_heap_head* addrHead;
	addrHead = (struct mem_heap_head*)((char*)addr - sizeof(struct mem_heap_head));
	return (void*)addrHead;
}

//private:判断控制字首部指针是否指向heap末尾
bool IsPtrEndOfHeap(void* ptr)
{
	struct mem_heap_head* head = (struct mem_heap_head*)ptr;
	if (head == (struct mem_heap_head*)memory_heap_end)
		return true;
	else
		return false;
}

//private:最小插入一帧(头部+数据部分)大小
int MinSizeOfFrame(size_t size)
{
	return size + sizeof(struct mem_heap_head);
}

//private:输入当前控制字地址和数据部分长度,给出下一个控制字地址
void* ToNextPtr(void* addr, size_t size)
{
	if (addr == nullptr)
		return nullptr;
	return (struct mem_heap_head*)((char*)addr + MinSizeOfFrame(size));
}

void MyMemoryHeapFree(void* addr)
{
	//1.先根据addr找到对应的struct地址,地址是有小到大的
	struct mem_heap_head* addrHead;
	addrHead = (struct mem_heap_head *)ToHeadPtr(addr);
	addrHead->IsFreeData = false;
}

void* MyMemoryHeapMalloc(size_t size)
{
	//采用fast fit方式快速找到待分配的内存,同时完成内存碎片的优化
	struct mem_heap_head* head = (struct mem_heap_head*)memory_heap_start;
	while (IsPtrEndOfHeap(head) == false)
	{
		if (head->IsFreeData == true)
		{
			if (MinSizeOfFrame(size) <= head->MemHeapDataLength)	//需要对这一个内存块进行划分
			{
				struct mem_heap_head* nextHead = (struct mem_heap_head*)ToNextPtr(head, size);
				nextHead->IsFreeData = true;
				nextHead->MemHeapDataLength = head->MemHeapDataLength - MinSizeOfFrame(size);
				head->IsFreeData = false;
				head->MemHeapDataLength = size;
			}
			else    //一块内存块不够,需要从第二块开始合并
			{
				struct mem_heap_head* tempHead = head;
				long tempLength = tempHead->MemHeapDataLength;
				while (ToNextPtr(tempHead, tempHead->MemHeapDataLength) != memory_heap_end && ((struct mem_heap_head*)ToNextPtr(tempHead, tempHead->MemHeapDataLength))->IsFreeData == true && tempLength < MinSizeOfFrame(size))
				{
					tempLength += MinSizeOfFrame(((struct mem_heap_head*)ToNextPtr(tempHead, tempHead->MemHeapDataLength))->MemHeapDataLength);
					tempHead = (struct mem_heap_head*)ToNextPtr(tempHead, tempHead->MemHeapDataLength);
				}
				if (tempLength >= MinSizeOfFrame(size))
				{
					head->IsFreeData = false;
					head->MemHeapDataLength = tempLength;
					return (char*)head + sizeof(head);
				}
			}
		}
		else
			head = (struct mem_heap_head*)ToNextPtr(head, head->MemHeapDataLength);
	}
	return nullptr;
}

这里在free的时候并没有去做内存回收,为啥呢?原因很简单,因为这个时候回收的效果不好,且有风险。

比较棒的内存池分配方式

内存池的分配方式呢,我在Linux里看到过,在Lwip协议的实现中也看到过,感觉确实不错,咱们先简单规划一下如何设计一个内存池:

#pragma once

#ifndef		_MEM_POOL_H_
#define		_MEM_POOL_H_

#include <list>
#include <mutex>

//0.一些基本的宏定义
constexpr auto TOTAL_SIZE_PER_BLOCK = 2*1024*1024;
constexpr auto POOL_COUNTS = 10;
constexpr auto MAX_MALLOC_KIND_OF_SIZE = 20;

//1.先将目前支持的尺寸列出来
enum KindOfSize {size4B = 2, size16B = 4, size64B = 6, size256B = 8, size1K = 10, size4K = 12, size16K = 14, size64K = 16, size256K = 18, size1M = 20};

//2.定义一下内存池的基本单元:内存块
struct MemBlock {
	KindOfSize perSize;
	size_t	counts;
	void* startAddr;
	std::list<int> free_units;
	std::list<int> used_units;
	MemBlock(KindOfSize sz, size_t cnts) :perSize(sz), counts(cnts) {}
};

//3.定义最后的内存池以及内存池操作
class MemPool {
private:
	std::list<struct MemBlock*> mem_pool;
	std::mutex	mx;			//用于保护内存不被同时访问,我暂时还没加上去,我建议你帮我加上去。
public:
	MemPool()
	{
		for (int i = 0; i < POOL_COUNTS; ++i)
		{
			struct MemBlock* pMb = new struct MemBlock((KindOfSize)(2*i+2), (size_t)(TOTAL_SIZE_PER_BLOCK / pow(2, 2*i+2)));
			pMb->startAddr = new char(TOTAL_SIZE_PER_BLOCK);
			for (int j = 0; j < (size_t)(TOTAL_SIZE_PER_BLOCK / pow(2, 2*i+2)); ++j)
				pMb->free_units.push_back(j);
			pMb->used_units.clear();
			mem_pool.push_back(pMb);
		}
	}
	~MemPool()
	{
		//这一块我懒得写了,各位兄弟帮我补上去
	}
public:
	void* MyMalloc(size_t size)
	{
		//1.先确定用哪个内存块尺寸,不能超出最大内存块尺寸
		int targetSize = 2;
		while (1)
		{
			if (size > pow(2, targetSize))
				targetSize += 2;
			else
				break;
		}
		if (targetSize > MAX_MALLOC_KIND_OF_SIZE)
		{
			std::cout << "申请单个内存大小超过范围!" << std::endl;
			return nullptr;
		}
		//2.寻找目标内存块并耗费一块free unit
		for (auto iter = mem_pool.begin(); iter != mem_pool.end(); ++iter)
		{
			if ((*iter)->perSize == (KindOfSize)targetSize && (*iter)->free_units.empty() == false)
			{
				int offset = (*iter)->free_units.back();
				(*iter)->free_units.pop_back();
				(*iter)->used_units.push_back(offset);
				std::cout << "内存申请成功!地址为:" << (void*)((char*)(*iter)->startAddr + offset * (*iter)->perSize) << std::endl;
				return (void*)((char*)(*iter)->startAddr + offset * (*iter)->perSize);
			}
		}
		//3.没找到合适的内存,需要再手动创建一份内存块并加入到内存池中,同时消耗一块free unit
		struct MemBlock* pMb = new struct MemBlock((KindOfSize)(targetSize), (size_t)(TOTAL_SIZE_PER_BLOCK / pow(2, targetSize)));
		pMb->startAddr = new char(TOTAL_SIZE_PER_BLOCK);
		for (int j = 0; j < (size_t)(TOTAL_SIZE_PER_BLOCK / pow(2, targetSize)); ++j)
			pMb->free_units.push_back(j);
		pMb->used_units.clear();
		mem_pool.push_back(pMb);
		int offset = (mem_pool.back())->free_units.back();
		(mem_pool.back())->free_units.pop_back();
		(mem_pool.back())->used_units.push_back(offset);
		std::cout << "单独开辟了一块内存块!内存申请成功!地址为:" << (void*)((char*)(mem_pool.back())->startAddr + offset * (mem_pool.back())->perSize) << std::endl;
		return (void*)((char*)(mem_pool.back())->startAddr + offset * (mem_pool.back())->perSize);
	}
	void MyFree(void* addr)
	{
		for (auto iter = mem_pool.begin(); iter != mem_pool.end(); ++iter)
		{
			if ((char*)addr >= (char*)(*iter)->startAddr && (char*)addr < (char*)(*iter)->startAddr + (*iter)->perSize * (*iter)->counts)
			{
				for (auto ineriter = (*iter)->used_units.begin(); ineriter != (*iter)->used_units.end(); ++ineriter)
				{
					if ( ((char*)addr - (char*)(*iter)->startAddr) / (*iter)->perSize == *ineriter )
					{
						(*iter)->free_units.push_back(*ineriter);
						ineriter = (*iter)->used_units.erase(ineriter);
						break;
					}
				}
				break;
			}
		}
		return;
	}
};

#endif

假如你仔细看完了我写的代码,你会发现一件很僵硬的事情:凭什么我单次申请超过1M内存就不能成功???这是啥道理???归根到底,咱们不支持一次分配多个块。Linux的内存管理中有这样的一个结构,叫Arena,这个Arena就可以用来帮助咱们解决,内存超过1M时的分配问题。
当申请内存大小超过1M时,我们就设置一个内存池,里面的单位就是1M为一个unit,然后每次申请时都会将最开始的一块地址上设置成arena,arena上写着从此开始的后续多少块,属于一组数据。
大概就是提这么一下,喜欢折腾的朋友可以动手实现一下,或者异步Linux源码看看。

C++ new的几个版本

今天刷面经看到了operator new和placement new,就有点懵逼,这里讲清楚这些是啥。
首先咱们得明确一个概念就是,new的时候究竟干了啥。
1.执行operator new运算,开辟内存空间
2.执行构造对象的构造函数
3.返回申请到的内存地址
其实咱们看看,第一步就是调用了系统实现的operator new操作,咱们可以找到这个实现代码看看有多少版本:

throwing (1)    
void* operator new (std::size_t size) throw (std::bad_alloc);
nothrow (2) 
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
placement (3)   
void* operator new (std::size_t size, void* ptr) throw();

咱们之前经常说,哎哎我要重载new,重载的是new的第一步操作,operator new!!!
看完代码之后你发现了,原来系统默认提供了三种实现方式,分别是:带有error返回的、不带error返回的、placement方式。
咱们说的placement new其实就是第三种方式,它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。这tm就很离谱,居然嫌弃new操作太多了,直接把第一步给去掉了,默认程序员已经申请好了内存了,于是placement第一步啥也不做,这样的new相当于执行了第二步和第三步,嘿嘿嘿。
估计你现在很好奇,为啥要有placement new这种东西。其实这样一来,就能在一块内存上重复使用了不是么?

placement new的正确使用姿势

1.分配内存

char *  buff  =   new   char [  sizeof (Foo)  *  N ];
memset( buff,  0 ,  sizeof (Foo) * N );

2.用placement new方式构建对象

Foo *  pfoo  =   new  (buff)Foo;

3.使用对象

pfoo -> print();

pfoo -> set_f( 1.0f );
pfoo -> get_f();

4.显式的调用对象的析构函数销毁对象

pfoo ->~ Foo();

5.重复执行上述2~4的流程即可达到重复使用而不必次次申请内存,美滋滋
6.销毁所有内存

delete [] buff;

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106524826
今日推荐