天天看點

SDL_Engine遊戲引擎制作 1-C++的記憶體自動釋放

衆所周知,C++本身是不會自動釋放new出來的對象的記憶體,即使沒有指針引用它(此時的記憶體在程式運作期間将無法釋放,導緻了記憶體洩漏)。cocos2d-x給出的解決辦法就是引用計數。SDL_Engine關于引用計數涉及到三個類,分别是:Object,PoolManager,AutoreleasePool。PoolManager是單例類,内有一個指向AutoreleasePool對象的指針(不同于cocos2dx),而AutoreleasePool有一個Object的指針數組,用來在一幀的結束時使得所有容器中的對象的引用數減少一。

先建立一個PlatformMarcos.h檔案 包括一些基本的宏。

#ifndef __Platform_H__
#define __Platform_H__

#include<assert.h>
#include<functional>
#include<cstdio>
//namespace
#define NS_SDL_BEGIN namespace SDL{
#define NS_SDL_END }
#define USING_NS_SDL using namespace SDL;

#define CREATE_FUNC(__TYPE__)    \
static __TYPE__*create()         \
{                                \
	__TYPE__*pRet=new __TYPE__();\
	if(pRet&&pRet->init())       \
	{                            \
         pRet->autorelease();   \
		 return pRet;            \
	}                            \
	delete pRet;                 \
	pRet=NULL;                   \
	return pRet;                 \
}
//斷言
#define SDLASSERT(cond,msg)        \
if(!(cond))                        \
{                                  \
	printf("Assert failed:%s",msg);\
	assert(cond);                  \
}

#define SDL_SAFE_DELETE(p)do{if(p) delete p;p=nullptr;}while(0)
#define SDL_SAFE_RELEASE(p)do{if(p) p->release();}while(0)
#define SDL_SAFE_RELEASE_NULL(p)do{if(p) p->release(); p = nullptr;}while(0)
#define SDL_SAFE_RETAIN(p) do { if(p) { (p)->retain(); } } while(0)
#define SDL_BREAK_IF(cond) if(cond) break

#endif
           

PlantformMarcos.h檔案包含了一些基本的宏,為友善以後的開發,後續也将會添加。需要注意的是,使用宏函數時應當添加盡可能多的小括号來保證其結果的正确性。

然後建立一個Object類,此類可以認為是java中的Object,以後的開發完全繼承自該類,即萬物皆為對象的思想。

#ifndef __Object_H__
#define __Object_H__

#include "PlatformMarcos.h"
NS_SDL_BEGIN

class AutoreleasePool;
/*拷貝接口*/
class Clonable
{
public:
	virtual Clonable *clone()const=0;
	//添加析構函數,不添加可能會發生記憶體洩漏
	virtual ~Clonable(){}
};

class Object
{
protected:
	//引用計數器
	unsigned int _referenceCount;
	//是否由記憶體管理器管理
	bool _managed;
	//唯一ID
	unsigned int _uniqueID;
public:
	Object();
	virtual ~Object();
	
	CREATE_FUNC(Object);
	bool init();
	//保留
	void retain();
	//釋放
	void release();
	//自動釋放
	Object*autorelease();
	//獲得引用數量
	unsigned int getReferenceCount() const{return _referenceCount;}
	//擷取目前的id
	unsigned int getUniqueID()const{return _uniqueID;}
	//是否交給釋放池
	bool isManaged(){return _managed;}
	//友元
	friend class AutoreleasePool;
};

NS_SDL_END
#endif
           

Clonable為抽象類,也可以認為是接口。嚴格地來說,c++中沒有類似于java中的關鍵字來特地聲明接口。

Object對象中含有一些威客保證能自動釋放所必須的屬性,引用計數的關鍵就在于retain(),release(),autorelease()函數。

#include "Object.h"
#include "PoolManager.h"

NS_SDL_BEGIN
Object::Object()
	:_uniqueID(0)
{
	//定義一個靜态變量作為執行個體對象引用器,使得每個對象的id都唯一
	static unsigned int uObjectCount=0;
	uObjectCount++;
	
	_referenceCount = 1;
	_managed = false;
	_uniqueID = uObjectCount;
}

Object::~Object()
{
	//避免重複删除造成的錯誤
	if(_managed)
	{
		PoolManager::getInstance()->removeObject(this);
	}
}

bool Object::init()
{
	return true;
}

void Object::retain()
{
	SDLASSERT(_referenceCount > 0,"引用數不能小于0");

	++_referenceCount;
}

void Object::release()
{
	SDLASSERT(_referenceCount > 0,"引用數不能小于0");

	--_referenceCount;
	//如果引用計數為0 釋放記憶體
	if(_referenceCount == 0)
		delete this;
}

Object* Object::autorelease()
{
	if(_managed == false)
	{
		//加入自動釋放池中
		PoolManager::getInstance()->addObject(this);
		_managed=true;
	}
	return this;
}

NS_SDL_END
           

如上所見,retain函數是使得引用計數加一,release函數則是減一。需要注意的是,當某個Object對象的引用為0時,就delete該對象,釋放其記憶體。autorelease函數則是把目前的對象交給自動釋放池,使得一幀結束後對内部包含的所有對象的引用計數全部減少一,并且不再管理此對象。比如,我在某處建立了一個Object* pObject = Object::create() (在create函數中内部調用了autorelease函數),然後沒有調用該對象的retain函數,則在一幀結束後,PoolManager就會釋放該記憶體。

#ifndef __AutoreleasePool_H__
#define __AutoreleasePool_H__
#include <vector>
#include <algorithm>

#include "Object.h"
NS_SDL_BEGIN
class AutoreleasePool : public Object
{
private:
	std::vector<Object*> _managedObjects;
public:
	AutoreleasePool();
	virtual ~AutoreleasePool();
	//insert different
	void addObject(Object*pObject);
	void removeObject(Object*pObject);
	//清理容器
	void clear();
};
NS_SDL_END
#endif
           

自動釋放池儲存着要管理的Object對象數組。

#include "AutoreleasePool.h"
NS_SDL_BEGIN

AutoreleasePool::AutoreleasePool(void)
{
}

AutoreleasePool::~AutoreleasePool(void)
{
}

void AutoreleasePool::addObject(Object*pObject)
{
	_managedObjects.push_back(pObject);
}

void AutoreleasePool::removeObject(Object*pObject)
{
	auto it = std::find(_managedObjects.begin(),_managedObjects.end(),pObject);
	//沒有該對象的引用,直接傳回
	if (it == _managedObjects.end())
		return;
	//不再被管理
	pObject->_managed = false;
	_managedObjects.erase(it);
}
void AutoreleasePool::clear()
{
	for(auto it = _managedObjects.begin();it != _managedObjects.end();)
	{
		auto object = *it;

		object->_managed = false;
		object->release();

		it = _managedObjects.erase(it);
	}
}
NS_SDL_END
           

Autorelease類中用的最多的就是clear函數。即周遊數組,并進行release後從容器中删除此對象。

而PoolManager則相對簡單,主要負責對Autorelease對象的管理,由于在SDL_Engine中,僅僅使用了一個Autorelease,是以該對象也可以删除。(為了擴充性而保留)

PoolManager為單例類。

#ifndef __SDL_PoolManager_H__
#define __SDL_PoolManager_H__

#include "Object.h"

NS_SDL_BEGIN
class AutoreleasePool;

class PoolManager:public Object
{
private:
	static PoolManager* _pInstance;
private:
	AutoreleasePool* _pCurReleasePool;
private:
	PoolManager();
	~PoolManager();
public:
	static PoolManager*getInstance();
	static void purge();

	AutoreleasePool* getCurReleasePool();

	void addObject(Object*pObject);
	void removeObject(Object*pObject);
};
NS_SDL_END
#endif
           
#include "PoolManager.h"
#include "AutoreleasePool.h"
NS_SDL_BEGIN

PoolManager*PoolManager::_pInstance=NULL;

PoolManager::PoolManager()
{
	//建立一個記憶體釋放池
	_pCurReleasePool=new AutoreleasePool();
}
PoolManager::~PoolManager()
{
	//删除目前棧内容
	_pCurReleasePool->clear();
	//釋放目前記憶體池
	SDL_SAFE_RELEASE(_pCurReleasePool);
}
PoolManager*PoolManager::getInstance()
{
	if(_pInstance==NULL)
	{
		_pInstance=new PoolManager();
	}
	return _pInstance;
}
void PoolManager::addObject(Object*pObject)
{
	_pCurReleasePool->addObject(pObject);
}
void PoolManager::removeObject(Object*pObject)
{
	SDLASSERT(_pCurReleasePool,"Object();");

	_pCurReleasePool->removeObject(pObject);
}
AutoreleasePool*PoolManager::getCurReleasePool()
{
	return _pCurReleasePool;
}
void PoolManager::purge()
{
	//釋放單例類
	SDL_SAFE_DELETE(_pInstance);
}
NS_SDL_END
           

PoolManager中主要就是引用了AutoreleasePool類中的函數。

以上是為了實作引用計數的而進行的編寫。接下來進行測試是否可以自動釋放,由于此時沒有使用到SDL,故目前還不存在幀的重新整理,故本節的測試在控制台進行。這裡建議下載下傳一個記憶體洩漏檢測工具,或者在Object的構造函數和析構函數中編寫輸出函數,來進行檢測。

建立main.cpp

#include <iostream>
#include "SDL_Engine.h"
#include "vld.h"//記憶體洩漏工具對應的頭檔案,可删除

USING_NS_SDL;
using namespace std;

int main()
{
	int key = 0;
	Object* object = Object::create();

	while ((key = getchar()) != 'q')
	{
		switch (key)
		{
		case '1':object->retain();break;
		case '2':object->release();break;
		default:
			break;
		}
		PoolManager::getInstance()->getCurReleasePool()->clear();
	}
	PoolManager::purge();
	_CrtDumpMemoryLeaks();//可删除
	return 0;
}
           

在main函數一開始,建立了一個Object對象,為了模拟遊戲中的幀,而采用了鍵盤響應。

測試一:輸入除1,2的其他字元,然後再輸入1或2,即報錯(我的vs2012沒有報錯,但是如果設定斷點的話,會發現該object對象的記憶體已經釋放)

測試二:成對地輸入1,2然後輸入q,記憶體無洩漏。

測試三:隻輸入1,之後輸入q,記憶體會發生洩漏。

本節代碼連結:https://pan.baidu.com/s/1ow-z7HafqCblCfQ19DQkRw 密碼:fuwd

下一節将會實作SDL的視窗和渲染器的建立。