内存管理

内存碎片

我们先来看看内存碎片是什么,是如何产生的。

假如我们系统只有20个连续字节给我们使用,执行以下代码:

void *p1 = malloc(1);
void *p2 = malloc(2);
void *p3 = malloc(2);
void *p4 = malloc(5);
void *p6 = malloc(1);
void *p7 = malloc(2);
void *p8 = malloc(2);
void *p9 = malloc(5);

free(p2);
free(p3);

free(p7);
free(p8);

我们的内存变成了:
在这里插入图片描述
空闲内存一共有8但是我们要malloc(5)却得不到,两个空闲4内存就是内存碎片。

做好内存管理,可以避免内存碎片的产生,使程序长期稳定、高效的运行。
我们用的操作系统也是会做内存的整理工作,不过并不能完全依赖操作系统来做这件事情,万一以后换了操作系统不做整理了,甚至要裸机开发的话还是用得上的,这也能有效的减少操作系统的负担。
还要一点就是操作系统并不知道我们程序需要如何管理内存比较高效(比如说常用数据有多大),所以我们需要自己来管理(特化一般都是比泛化快的)。

避免内存碎片的产生

内存池

从系统中申请足够大小的内存,由程序自己管理。

c和c++内存操作主要有new、new[]和malloc,new的话可以重载,malloc的话我们就直接不用换成我们自己设计的malloc函数(当然这里malloc替换之后并没有做什么额外的操作)。

下面的代码都是看的网课里面的。

void* operator new(size_t nSize)
{
	return MemoryMgr::Instance().allocMem(nSize);
}

void operator delete(void *p)
{
	MemoryMgr::Instance().freeMem(p);
}

void* operator new[](size_t nSize)
{
	return MemoryMgr::Instance().allocMem(nSize);
}

void operator delete[](void *p)
{
	MemoryMgr::Instance().freeMem(p);
}

void* mem_alloc(size_t size)
{
	return malloc(size);
}

void mem_free(void *p)
{
	free(p);
}

可以看到所有内容都封装在MemoryMgr。
MemoryMgr一共管理五种大小不同的分配器MemoryAlloctor(当然这可以自己定),其他大小的内存还是问系统要。
分配器MemoryAlloctor管理一块内存空间,划分成一个个内存块(有对应的描述信息),内存用完了还是问操作系统要。
在这里插入图片描述

MemoryBlock

我们先来看一下最小的内存块的描述信息。

  1. 因为我们有多个内存池,所以需要pAlloc来指定归属。
  2. pNext可以写成链表形式管理空闲块。
  3. nID目前算无用的信息,我们在调试的时候可以用来查看逻辑错误,实际应用的时候可以删掉。
  4. nRef表示我们的内存块被引用了几次。这个代码里面只有一次和零次。
  5. bPool用来指示内存是否归内存池管理。因为我们申请的大内存块用完了就会使用系统的,申请的内存太大也会使用系统的。
  6. 三个预留的char也可以去掉,如果没有内存块信息还是会对齐的。
// 内存块 最小单元
class MemoryBlock
{
public:
	// 所属大内存块(池)
	MemoryAlloc* pAlloc;
	// 下一块位置
	MemoryBlock* pNext;
	// 内存块编号
	int nID;
	// 引用次数
	int nRef;
	// 是否在内存池中
	bool bPool;
private:
	// 预留
	char c1;
	char c2;
	char c3;
};

分配器MemoryAlloctor

使用模板来指定分配器管理的内存单元大小nSize(按照指针大小对齐的)和内存单元数量nBlockSize。

// 便于在声明类成员变量时初始化MemoryAlloc的成员数据
template<size_t nSize, size_t nBlockSize>
class MemoryAlloctor :public MemoryAlloc
{
public:
	MemoryAlloctor()
	{
		//8 4   61/8=7  61%8=5
		const size_t n = sizeof(void*);
		//(7*8)+8 
		_nSize = (nSize / n)*n + (nSize % n ? n : 0);
		_nBlockSize = nBlockSize;
	}
};
申请内存池

内存池管理的内存(提供使用的+描述信息)直接问系统要。并且完善所有描述信息(类似单链表)。

void initMemory()
{
	xPrintf("initMemory:_nSize=%d,_nBlockSize=%d\n", _nSize, _nBlockSize);
	// 断言 不能重复申请
	assert(nullptr == _pBuf);
	if (_pBuf)
	{
		return;
	}

	// 计算内存池的大小
	size_t realSize = _nSize + sizeof(MemoryBlock);
	size_t bufSize = realSize*_nBlockSize;

	// 向系统申请池的内存
	_pBuf = (char*)malloc(bufSize);

	// 初始化内存池
	_pHeader = (MemoryBlock*)_pBuf;
	_pHeader->bPool = true;
	_pHeader->nID = 0;
	_pHeader->nRef = 0;
	_pHeader->pAlloc = this;
	_pHeader->pNext = nullptr;
	
	// 遍历内存块进行初始化
	MemoryBlock *pTemp1 = _pHeader;
	for (size_t n = 1; n < _nBlockSize; n++)
	{
		MemoryBlock *pTemp2 = (MemoryBlock*)(_pBuf + (n * realSize));
		pTemp2->bPool = true;
		pTemp2->nID = n;
		pTemp2->nRef = 0;
		pTemp2->pAlloc = this;
		pTemp2->pNext = nullptr;
		pTemp1->pNext = pTemp2;
		pTemp1 = pTemp2;
	}
}
申请内存

没申请内存池则initMemory();,如果内存池用完了就问系统要,负责直接分配(给用户的地址需要偏移一个描述信息大小)。

// 申请内存
void* allocMemory(size_t nSize)
{
	std::lock_guard<std::mutex> lg(_mutex);
	if (!_pBuf)
	{
		initMemory();
	}
		
	MemoryBlock *pReturn = nullptr;
	if (nullptr == _pHeader)	// 没有空闲块向系统要
	{
		pReturn = (MemoryBlock*)malloc(nSize + sizeof(MemoryBlock));
		pReturn->bPool = false;
		pReturn->nID = -1;
		pReturn->nRef = 1;
		pReturn->pAlloc = nullptr;
		pReturn->pNext = nullptr;
	}
	else // 有空闲块则分配
	{
		pReturn = _pHeader;
		_pHeader = _pHeader->pNext;
		assert(0 == pReturn->nRef);
		pReturn->nRef = 1;
	}
	//xPrintf("allocMem: %llx, id=%d, size=%d\n", pReturn, pReturn->nID, nSize);
	return ((char*)pReturn + sizeof(MemoryBlock));
}

可以看到这里上锁了,使用了lock_guard,看一下源代码就是lg变量创建的时候构造函数中_mutex lock一下,因为是lg局部变量,所以allocMemory结束的时候析构函数中_mutex unlock一下。

// CLASS TEMPLATE lock_guard
template<class _Mutex>
class lock_guard
{	// class with destructor that unlocks a mutex
public:
using mutex_type = _Mutex;

explicit lock_guard(_Mutex& _Mtx)
	: _MyMutex(_Mtx)
	{	// construct and lock
	_MyMutex.lock();
	}

lock_guard(_Mutex& _Mtx, adopt_lock_t)
	: _MyMutex(_Mtx)
	{	// construct but don't lock
	}

~lock_guard() noexcept
	{	// unlock
	_MyMutex.unlock();
	}

lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
释放内存

注意拿到内存的时候需要往前偏移一个描述信息的大小,然后根据bPool归属使用free或者放回内存池。
放回内存池操作也是上锁的。

void freeMemory(void* pMem)
{
	MemoryBlock *pBlock = (MemoryBlock*)((char*)pMem - sizeof(MemoryBlock));
	assert(1 == pBlock->nRef);
	if (pBlock->bPool)
	{
		std::lock_guard<std::mutex> lg(_mutex);
		if (--pBlock->nRef != 0)
		{
			return;
		}
		pBlock->pNext = _pHeader;
		_pHeader = pBlock;
	}
	else
	{
		if (--pBlock->nRef != 0)
		{
			return;
		}
		free(pBlock);
	}
}

MemoryMgr

单例模式

内存管理工具设计成单例模式方便使用(可能有人觉得视频里面的单例模式应该是线程不安全的,因为静态局部变量是懒汉模式行为,运行到的时候才加载,不过C++11是线程安全的):

class MemoryMgr
{
private:
	MemoryMgr()
	{
		init_szAlloc(0, 64, &_mem64);
		init_szAlloc(65, 128, &_mem128);
		init_szAlloc(129, 256, &_mem256);
		init_szAlloc(257, 512, &_mem512);
		init_szAlloc(513, 1024, &_mem1024);
		xPrintf("MemoryMgr\n");
	}

	~MemoryMgr()
	{

	}

public:
	// 单例模式 对象获取接口
	static MemoryMgr& Instance()
	{
		static MemoryMgr mgr;
		return mgr;
	}
	
	...	

};
内存池映射数组

我们在new的时候会传入要申请数据的大小,_szAlloc是一个指针数组,直接和MemoryAlloctor分配器建立映射关系,因为是从0开始映射的所以需要1024+1个。
以后申请内存的时候直接使用_szAlloc[nSize]就能找到对应的MemoryAlloctor分配器。

class MemoryMgr
{
private:
	MemoryMgr()
	{
		init_szAlloc(0, 64, &_mem64);
		init_szAlloc(65, 128, &_mem128);
		init_szAlloc(129, 256, &_mem256);
		init_szAlloc(257, 512, &_mem512);
		init_szAlloc(513, 1024, &_mem1024);
		xPrintf("MemoryMgr\n");
	}

private:

	// 初始化内存池映射数组
	void init_szAlloc(int nBegin, int nEnd, MemoryAlloc* pMemA)
	{
		for (int n = nBegin; n <= nEnd; n++)
		{
			_szAlloc[n] = pMemA;
		}
	}

private:
	MemoryAlloctor<64, 100000> _mem64;
	MemoryAlloctor<128, 100000> _mem128;
	MemoryAlloctor<256, 100000> _mem256;
	MemoryAlloctor<512, 100000> _mem512;
	MemoryAlloctor<1024, 100000> _mem1024;
	MemoryAlloc* _szAlloc[MAX_MEMORY_SIZE + 1];
};
内存块的引用计数

增加内存块的引用计数这里没用的上,不过可以用来添加内存池的其他功能,比如说共享内存什么的。

// 增加内存块的引用计数
void addRef(void* pMem)
{
	MemoryBlock *pBlock = (MemoryBlock*)((char*)pMem - sizeof(MemoryBlock));
	++pBlock->nRef;
}
申请内存

大于1024直接malloc(这里申请的内存也会加上描述信息,主要是bPool = false,并且给用户的地址需要 + sizeof(MemoryBlock)),不然使用分配器管理。

void* allocMem(size_t nSize)
{
	if (nSize <= MAX_MEMORY_SIZE)	// 内存池管理
	{
		return _szAlloc[nSize]->allocMemory(nSize);
	}
	else // 问系统要
	{
		MemoryBlock *pReturn = (MemoryBlock*)malloc(nSize + sizeof(MemoryBlock));
		pReturn->bPool = false;
		pReturn->nID = -1;
		pReturn->nRef = 1;
		pReturn->pAlloc = nullptr;
		pReturn->pNext = nullptr;
		//xPrintf("allocMem: %llx, id=%d, size=%d\n", pReturn , pReturn->nID, nSize);
		return ((char*)pReturn + sizeof(MemoryBlock));
	}
}
释放内存

注意拿到内存的时候需要往前偏移一个描述信息的大小,然后根据bPool归属使用free或者分配器释放。判断引用了几次if (--pBlock->nRef == 0)在这个代码也是可以不用的,可以扩展功能。

void freeMem(void *pMem)
{
	MemoryBlock *pBlock = (MemoryBlock*)((char*)pMem - sizeof(MemoryBlock));
	//xPrintf("freeMem: %llx, id=%d\n", pBlock, pBlock->nID);
	if (pBlock->bPool)
	{
		pBlock->pAlloc->freeMemory(pMem);
	}
	else 
	{
		if (--pBlock->nRef == 0)
		{
			free(pBlock);
		}
	}
}

测试代码

我们创建8个线程,每个线程都做内存申请和释放,加上头文件#include"Alloctor.h"则使用我们的内存池,注释掉则不使用。
我测试下来和系统管理的消耗差不多。

//#include "Alloctor.h"
#include <stdlib.h>
#include <iostream>
#include <thread>
#include <mutex>
#include "CELLTimestamp.hpp"
using namespace std;

const int tCount = 8;
const int mCount = 100000;
const int nCount = mCount/tCount;

void workFun(int index)
{
	char* data[nCount];
	for (size_t i = 0; i < nCount; i++)
	{
		data[i] = new char[(rand() % 128) + 1];
	}
	for (size_t i = 0; i < nCount; i++)
	{
		delete[] data[i];
	}
}

int main()
{
	thread t[tCount];
	for (int n = 0; n < tCount; n++)
	{
		t[n] = thread(workFun, n);
	}
	CELLTimestamp tTime;
	for (int n = 0; n < tCount; n++)
	{
		t[n].join();
		//t[n].detach();
	}
	cout << tTime.getElapsedTimeInMilliSec() << endl;
	cout << "Hello,main thread." << endl;
	return 0;
}

对象池

创建足够多的对象,减少创建释放对象的消耗。
当我们动态管理比如说登陆用户这种类的时候,用户登陆需要new对象,登出需要delete对象,我们就可以使用对象池来减少开销。

我们每个类都可以单独管理自己的对象池,需要类重载new运算符。管理思想和内存池类似。

在这里插入图片描述

对象池基类

重载new和delete运算符,并且提供原本的new和delete操作接口。

template<class Type, size_t nPoolSzie>
class ObjectPoolBase
{
public:

	// 重载 new 和 delete
	void* operator new(size_t nSize)
	{
		return objectPool().allocObjMemory(nSize);
	}

	void operator delete(void* p)
	{
		objectPool().freeObjMemory(p);
	}

	// 提供创建和销毁对象的方法
	template<typename ...Args>
	static Type* createObject(Args ... args)
	{
		Type* obj = new Type(args...);

		return obj;
	}

	static void destroyObject(Type* obj)
	{
		delete obj;
	}
private:
	// 对象池
	typedef CELLObjectPool<Type, nPoolSzie> ClassTypePool;
	static ClassTypePool& objectPool()
	{
		static ClassTypePool sPool;
		return sPool;
	}
};

对象描述信息

class NodeHeader
{
public:
	// 下一块位置
	NodeHeader* pNext;
	// 内存块编号
	int nID;
	// 引用次数
	char nRef;
	// 是否在内存池中
	bool bPool;
private:
	// 预留
	char c1;
	char c2;
};

初始化对象池

void initPool()
{
	// 断言 不能重复申请
	assert(nullptr == _pBuf);
	if (_pBuf)
	{
		return;
	}

	// 计算对象池的大小
	size_t realSize = sizeof(Type) + sizeof(NodeHeader);
	size_t n = nPoolSize*realSize;

	// 申请池的内存
	_pBuf = new char[n];

	// 初始化内存池
	_pHeader = (NodeHeader*)_pBuf;
	_pHeader->bPool = true;
	_pHeader->nID = 0;
	_pHeader->nRef = 0;
	_pHeader->pNext = nullptr;

	// 遍历内存块进行初始化
	NodeHeader *pTemp1 = _pHeader;
	for (size_t n = 1; n < nPoolSize; ++n)
	{
		NodeHeader *pTemp2 = (NodeHeader*)(_pBuf + (n* realSize));
		pTemp2->bPool = true;
		pTemp2->nID = n;
		pTemp2->nRef = 0;
		pTemp2->pNext = nullptr;
		pTemp1->pNext = pTemp2;
		pTemp1 = pTemp2;
	}
}

申请对象内存

void* allocObjMemory(size_t nSize)
{
	std::lock_guard<std::mutex> lg(_mutex);
	NodeHeader *pReturn = nullptr;
	if (nullptr == _pHeader)
	{
		pReturn = (NodeHeader*)new char[sizeof(Type) + sizeof(NodeHeader)];
		pReturn->bPool = false;
		pReturn->nID = -1;
		pReturn->nRef = 1;
		pReturn->pNext = nullptr;
	}
	else
	{
		pReturn = _pHeader;
		_pHeader = _pHeader->pNext;
		assert(0 == pReturn->nRef);
		pReturn->nRef = 1;
	}
	xPrintf("allocObjMemory: %llx, id=%d, size=%d\n", pReturn, pReturn->nID, nSize);
	return ((char*)pReturn + sizeof(NodeHeader));
}

释放对象内存

void freeObjMemory(void* pMem)
{
	NodeHeader *pBlock = (NodeHeader*)((char*)pMem - sizeof(NodeHeader));
	xPrintf("freeObjMemory: %llx, id=%d\n", pBlock, pBlock->nID);
	assert(1 == pBlock->nRef);
	if (pBlock->bPool)
	{
		std::lock_guard<std::mutex> lg(_mutex);
		if (--pBlock->nRef != 0)
		{
			return;
		}
		pBlock->pNext = _pHeader;
		_pHeader = pBlock;
	}
	else
	{
		if (--pBlock->nRef != 0)
		{
			return;
		}
		delete[] pBlock;
	}
}

测试代码

#include "Alloctor.h"
#include <stdlib.h>
#include <iostream>
#include <thread>
#include <mutex>
#include "CELLTimestamp.hpp"
#include "CELLObjectPool.hpp"
using namespace std;

class ClassA : public ObjectPoolBase<ClassA, 100000>
{
public:
	ClassA(int n)
	{
		num = n;
		printf("ClassA\n");
	}

	~ClassA()
	{
		printf("~ClassA\n");
	}
public:
	int num = 0;
};

class ClassB : public ObjectPoolBase<ClassB, 10>
{
public:
	ClassB(int n, int m)
	{
		num = n * m;
		printf("ClassB\n");
	}

	~ClassB()
	{
		printf("~ClassB\n");
	}
public:
	int num = 0;
};

int main()
{
	{
		ClassA *B1 = new ClassA(0);	// 使用对象池但不会自动释放
		shared_ptr<ClassA> s0 = make_shared<ClassA>(5);	// 不使用对象池 会自动释放
		shared_ptr<ClassA> s1(new ClassA(5));	// 使用对象池并且会自动释放
	}

	printf("----1----\n");
	{
		shared_ptr<ClassA> s1 = make_shared<ClassA>(5);
	}

	printf("----2----\n");
	{
		shared_ptr<ClassA> s1(new ClassA(5));
	}

	printf("----3----\n");
	ClassA* a1 = new ClassA(5);
	delete a1;

	printf("----4----\n");
	return 0;
}

运行结果:
在这里插入图片描述

避免内存泄露

在C++中,动态内存的管理是由程序员自己申请和释放的,使用堆内存是非常频繁的操作,容易造成堆内存泄露、二次释放等问题,为了更加容易和更加安全的使用动态内存,C++11中引入了智能指针的概念,方便管理堆内存,使得自动、异常安全的对象生存期管理可行。

智能指针主要思想是RAII思想,“使用对象管理资源”,在类的构造函数中获取资源,在类的析构函数中释放资源。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。

RAII是Resource Acquisition Is Initialization的简称,即资源获取就是初始化:

  1. 定义一个类来封装资源的分配与释放;
  2. 构造函数中完成资源的分配及初始化;
  3. 析构函数中完成资源的清理,可以保证资源的正确初始化和释放;
  4. 如果对象是用声明的方式在栈上创建局部对象,那么RAII机制就会正常工作,当离开作用域对象会自动销毁而调用析构函数释放资源。

智能指针

智能指针在C++11版本之后提供,包含在头文件<memory>中,标准命名std空间下,有auto_ptr、shared_ptr、weak_ptr、unique_ptr四种,其中auto_ptr已被弃用。

  • auto_ptr:拥有严格对象所有权语义的智能指针;
  • shared_ptr:拥有共享对象所有权语义的智能指针;
  • weak_ptr:到 shared_ptr 所管理对象的弱引用;
  • unique_ptr:拥有独有对象所有权语义的智能指针。

auto_ptr

auto_ptr是通过由 new 表达式获得的对象,并在auto_ptr自身被销毁时删除该对象的智能指针,它可用于为动态分配的对象提供异常安全、传递动态分配对象的所有权给函数和从函数返回动态分配的对象,是一个轻量级的智能指针,适合用来管理生命周期比较短或者不会被远距离传递的动态对象,最好是局限于某个函数内部或者是某个类的内部。

auto_ptr是最早期的智能指针,在C++11 中已被弃用,C++17 中移除,建议使用unique_ptr代替auto_ptr。

shared_ptr

shared_ptr多个指针指向相同的对象,也叫共享指针。shared_ptr采用了引用计数的方式,更好地解决了赋值与拷贝的问题,每一个shared_ptr的拷贝都指向相同的内存,每拷贝一次内部的引用计数加1,每析构一次内部的引用计数减1,为0时自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取时需要加锁。因为 shared_ptr 有两个数据成员,读写操作不能原子化。

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,用于专门解决shared_ptr循环引用的问题,因为它不具有普通指针的行为,没有重载operator * 和 ->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。weak_ptr可以使用一个非常重要的成员函数lock(),从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。

unique_ptr

unique_ptr实际上相当于一个安全性增强了的auto_ptr。unique_ptr是通过指针占有并管理另一对象,并在unique_ptr离开作用域时释放该对象的智能指针。unique_ptr的使用标志着控制权的转移,同一时刻只能有一个unique_ptr指向给定对象,通过禁止拷贝语义、只有移动语义来实现。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。

总结

智能指针就是模拟指针动作的类,一般智能指针都会重载 -> 和 * 操作符。智能指针主要作用是管理动态内存的释放。

  1. 不要使用std::auto_ptr;
  2. 当你需要一个独占资源所有权的指针,且不允许任何外界访问,请使用std::unique_ptr;
  3. 当你需要一个共享资源所有权的指针,请使用std::shared_ptr;
  4. 当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr;
  5. 不能把一个原生指针交给两个智能指针对象管理。

代码链接

百度云链接:https://pan.baidu.com/s/17auh6R7Hx0JSCxOYiZfTWw
提取码:70f4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值