자습
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;
}

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