内存分配方式介绍
前言
关于内存分配方式,这可是面试官最喜欢问的问题了。前几天笔试一家公司的我也被问到了这样的问题。难么?朋友,肯定不难,咱们今天就把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;