자습

SharedPtr, Reference Counting 구현과 이해

hyrule 2022. 5. 8. 00:52

 

 

 

 

//Class CRef
//Ref.h


/*
[스마트 포인터]

* 파이썬 또는 자바의 경우 가비지 컬렉터라는 프로그램이 계속 작동함.
	- 댕글링 포인터들을 자동으로 청소 but 속도가 느림.

* 스마트 포인터 - 공유 포인터
	- 원래 기본 기능으로 지원하지만 게임엔진 등에서는
	직접 만들어서 쓰는 편이다.

* 어떤 주소를 3개의 포인터가 참조하다가 한 포인터에서 delete를 했을 시
나머지 2개 포인터는 댕글링 포인터가 되어 버린다. 매우 위험한 상황.

* 그래서 레퍼런스 카운터라는 방법을 사용한다.
	- 모든 주소는 RC라는 값을 가지고 있고,
	- delete를 할때 RC값이 0이 아니면 할당을 해제하는 것이 아니라
	RC값을 하나 줄인다.
	- 위의 상황에서 한 포인터에서 delete를 할 경우에
	주소가 할당 해제가 되는 것이 아니라 RC가 2가 되는 방식이다.
*/


#pragma once

#include <string>

class CRef
{
public:
	CRef();
	CRef(const CRef& ref);
	virtual ~CRef();



public:
	//참조 카운트수 증가/감소 및 0이면 delete해주는 함수 추가
	void AddRef();
	void Release();

protected:
	int m_RefCount;

	//cf)size_t = unsigned __int64
	size_t	m_TypeID;
	std::string m_Name;
	std::string m_TypeName;
	bool m_Enable; //활성/비활성
	bool m_Active; //살아있는지 죽었는지

public:
	size_t	GetTypeID()	const
	{
		return m_TypeID;
	}

	const std::string& GetTypeName() const
	{
		return m_Name;
	}

	//같은 타입인지 체크
	template <typename T>
	bool CheckTypeID() const
	{
		return m_TypeID == typeid(T).hash_code();
	}

	template <typename T>
	void SetTypeID()
	{
		//타입 이름을 문자열 형태로 받아온다.
		//int ->"int",class A -> "class A"
		m_TypeName = typeid(T).name();

		//각 타입은 고유 번호를 가지고 있다. 이 번호를 가져온다.
		m_TypeID = typeid(T).hash_code();
	}
	void SetName(const std::string& name)
	{
		m_Name = name;
	}

	bool GetEnable() const
	{
		return m_Enable;
	}
	void SetEnable(bool Enable)
	{
		m_Enable = Enable;
	}

	bool GetActive() const
	{
		return m_Active;
	}

	void SetActive(bool Active)
	{
		m_Active = Active;
	}

};
//Class CRef
//Ref.cpp


#include "Ref.h"
#include <iostream>

CRef::CRef() :
	m_RefCount(0),
	m_Enable(true),
	m_Active(true),
	m_TypeID(0)
{
}

CRef::CRef(const CRef& ref) :
	m_RefCount(0),
	m_TypeName(ref.m_TypeName),
	m_TypeID(ref.m_TypeID),
	m_Enable(ref.m_Enable),
	m_Active(ref.m_Active)
{
	std::cout << "* CRef 생성자 호출" << std::endl;
}

CRef::~CRef()
{
	std::cout << "* CRef 소멸자 호출" << std::endl;
}

void CRef::AddRef()
{
	std::cout << "* 레퍼런스 카운팅 증가 했습니다" << std::endl;
	++m_RefCount;
}

void CRef::Release()
{
	std::cout << "* 레퍼런스 카운팅 감소 했습니다" << std::endl;
	--m_RefCount;


	//참조 수가 없다고 확인되면 자기 자신을 삭제
	if (m_RefCount <= 0)
	{
		//자기 자신을 삭제하는 함수는 문제가 되지 않는다.
		//함수는 만들어진 객체가 없어도 존재하기 때문.
		//클래스 포인터가 nullptr이어도 함수는 호출이 가능하다.
		//클래스 일반 멤버변수가 들어오면 문제가 발생하지만
		//그렇지 않으면 문제가 되지 않는다.
		std::cout << "* 레퍼런스 카운트가 없으므로 이 클래스의 할당을 해제합니다." << std::endl;
		delete this;
		return;
	}
}

 

//SharedPtr.h

#pragma once

#include <iostream>

/*
[SharedPtr.h]
* Ref(참조 카운팅) 클래스와 같이 활용하는 클래스
* 앞으로 포인터 변수를 SharedPtr 클래스로 대체한다.
* SharedPtr 클래스는 생성 이후 포인터와 동일하게 사용 하능

*/

template <typename T>
class CSharedPtr
{
public:
	CSharedPtr() :
		m_Ptr(nullptr)
	{

	}

	//복사 생성자
	CSharedPtr(const CSharedPtr<T>& ptr)
	{
		m_Ptr = ptr.m_Ptr;

		if (m_Ptr)
			m_Ptr->AddRef();
	}

	CSharedPtr(T* ptr)
	{
		m_Ptr = ptr;

		if (m_Ptr)
			m_Ptr->AddRef();
	}


	~CSharedPtr()
	{
		if (m_Ptr)
			m_Ptr->Release();
	}

private:
	T* m_Ptr;


public:

	//기존에 포인터를 가지고 있던 주소에 새로운 주소를 '대입'
	//클래스 형태로 비교를 할 수도 있고
	//아예 포인터 주소를 비교할 수도 있으므로
	void operator = (const CSharedPtr<T>& ptr)
	{
		//기존에 참조하고 있던 객체가 있을 경우 카운트를 1 감소한다.
		if (m_Ptr)
			m_Ptr->Release();


		//새로운 주소를 등록
		m_Ptr = ptr.m_Ptr;

		//m_Ptr이 nullPtr이 아닐 경우 자신을 레퍼런스 카운트팅을 늘려준다.
		if (m_Ptr)
		{
			m_Ptr->AddRef();
		}
	}
	void operator = (T* ptr)
	{
		if (m_Ptr)
			m_Ptr->Release();

		m_Ptr = ptr;

		if (m_Ptr)
			m_Ptr->AddRef();
	}

	bool operator == (const CSharedPtr<T>& ptr) const
	{
		return m_Ptr == ptr.m_Ptr;
	}
	bool operator == (T* ptr) const
	{
		return m_Ptr == ptr;
	}

	bool operator != (const CSharedPtr<T>& ptr) const
	{
		return m_Ptr != ptr.m_Ptr;
	}
	bool operator != (T* ptr) const
	{
		return m_Ptr != ptr;
	}


	operator T* () const
	{
		return m_Ptr;
	}

	T* operator ->() const
	{
		return m_Ptr;
	}

	T* operator * () const
	{
		return m_Ptr;
	}

	T* Get() const
	{
		return m_Ptr;
	}

};

 

 

 

//TestClass.h
//실험에 사용될 테스트용 클래스.

#pragma once
#include "Ref.h"
#include <iostream>


//공유 포인터를 사용할 클래스는 레퍼런스 클래스를 부모로 상속받아 준다.
class CTestClass :
    public CRef
{
public:
    CTestClass()
    {
        std::cout << "* 테스트 클래스 생성자 호출" << std::endl;

        //Ref 클래스를 상속받으면, 생성자에 SetTypeID를 해 준다.
        SetTypeID<CTestClass>();
    }

    ~CTestClass()
    {
        std::cout << "* 테스트 클래스 소멸자 호출" << std::endl;
    }

    void Test()
    {
        std::cout << "* 테스트 클래스의 Test() 함수 호출" << std::endl;
    }

};

 

 

 

//main.cpp
//작성한 SharedPtr 코드 작동을 확인하기 위한 코드


#include "Ref.h"
#include "SharedPtr.h"
#include "TestClass.h"


int main()
{
	//코드블럭 안에 생성해준다
	//코드블럭 밖으로 나가면 여기 있는 변수들은 모두 사용할 수 없게 된다.
	//일반적인 상황이라면 CTestClass 메모리 할당이 해제되지 않아서
	//댕글링 포인터가 발생한다.
	{
		//포인터는 공유 포인터로 생성해준다.
		CSharedPtr<class CTestClass> TestClass1 = new CTestClass;

		CSharedPtr<class CTestClass> TestClass2 = *TestClass1;

		CSharedPtr<class CTestClass> TestClass3 = TestClass2;

		TestClass2->Test();

		std::cout << TestClass3->GetTypeName() << std::endl;

	}

	std::cout << "\n* 메인함수 종료. 이 문구가 나오기 전에 할당이 해제되어야 정상 작동." << std::endl;
	return 0;
}

 

 

메모리 릭 없이 코드가 종료되는 것을 확인 가능하다.