*** 공부 방법 ***
1. 코딩을 해야 하는 부분은 첫 부분에 변수나 함수, 메소드에 대한 선언이 코드블럭으로 표시되어 있다. //ex) MakeFunction(); 2. 코드블럭 하단에는 해당 선언에 대한 구현 로직이 작성되어 있다. 처
hyrule.tistory.com
[사전 지식]
<일반 포인터>
동적할당한 객체는 직접 다시 동적할당을 해제해주어야 한다.
만약 여러 포인터가 같은 주소를 가르키고 있었는데 할당이 해제가 되었다면,
남아있는 포인터 변수들은 Dangling Pointer가 되어 버린다.
<참조 포인터와 레퍼런스 카운트>
하지만 참조 카운팅 방식을 사용하면,
동적할당된 객체는 자신이 몇 개의 포인터로부터 참조가 되고 있는지를 카운트한다.
각 포인터 변수가 사라진다면(소멸자가 호출된다면),
소멸자에서 참조하고 있는 객체의 Reference Count에 접근하여 숫자를 하나 내린다.
이 카운트가 0이 되면, 해당 객체는 알아서 자신의 소멸자를 호출(delete this)한다.
수동으로 delete를 해줄 필요가 없어지므로, 메모리 누수 관리가 용이해진다.
* 참고 자료: https://hyrule.tistory.com/95
SharedPtr, Reference Counting 구현과 이해
//Class CRef //Ref.h /* [스마트 포인터] * 파이썬 또는 자바의 경우 가비지 컬렉터라는 프로그램이 계속 작동함. - 댕글링 포인터들을 자동으로 청소 but 속도가 느림. * 스마트 포인터 - 공유 포인터 -
hyrule.tistory.com
<size_t>
자료형 - unsigned __int64 변수 타입과 동일.
<string>
- 헤더
- C++에서 지원하는 문자열
- std::string Name; 과 같이 선언하여 문자열처럼 사용할 수 있다.
<typeinfo>
- 헤더
- typeid(T).name(): 문자열로 해당 T타입의 이름을 문자열로 반환한다.
- typeid(T).hash_code(): 해당 T타입의 해쉬코드를 반환한다.
가장 먼저, CRef 클래스에서 사용하는 헤더를 GameInfo 헤더에 포함시킨다.
-> 문자열 헤더, typeinfo 헤더
//문자열
#include <string>
//타입 정보
#include <typeinfo>
class CRef
GameManager 필터 아래에 새 필터를 만들고 CRef 클래스를 생성한다
CRef 클래스는 정수 형태의 m_RefCount를 들고 있다.
이 변수는 다른 클래스에서 접근할 수 없다.
int m_RefCount;
CRef::CRef()
자기 자신에 대한 복사 생성자도 하나 만들어준다
중요한 건 복사되어서 생성되어도 주소는 새로 만들어지므로, 레퍼러스 카운트는 0이 되어야 한다.
m_Name 같은 경우 복사해서 생성된 다른 클래스가 이름이 같으면 구분히 힘들어지므로, 복사하지 않는다.
CRef::CRef(const CRef& ref):
m_RefCount(0),
m_TypeName(ref.m_TypeName),
m_TypeID(ref.m_TypeID)
{
}
CRef::AddRef()
현재 클래스의 Reference Count를 하나 올려주는 메소드
void CRef::AddRef()
{
++m_RefCount;
}
CRef::Release()
현재 클래스의 Reference Count를 하나 내리는 메소드.
더불어 자신의 Reference Count가 0 이하가 되면 자기 자신을 제거하는 기능도 추가한다.
void CRef::Release()
{
--m_RefCount;
//참조 수가 없다고 확인되면 자기 자신을 삭제
if (m_RefCount <= 0)
{
//자기 자신을 삭제하는 함수는 문제가 되지 않는다.
//함수는 만들어진 객체가 없어도 존재하기 때문.
//클래스 포인터가 nullptr이어도 함수는 호출이 가능하다.
//클래스 일반 멤버변수가 들어오면 문제가 발생하지만
//그렇지 않으면 문제가 되지 않는다.
delete this;
}
}
* CF) 자기 자신의 클래스를 제거하는 클래스 멤버 메소드는 에러가 안 날까?
-> 클래스 안의 메소드는 해당 클래스의 생성/삭제와 관계 없이 계속 주소를 가지고 메모리상에 위치하고 있다.
클래스를 제거해도 문제 없다.
CF2)
클래스 안의 메소드는 해당 클래스의 생성/삭제와 관계 없이 계속 주소를 가지고 메모리상에 위치하고 있다.
B메소드의 주소만 알고 있다면 어디서든지 접근할 수 있다는 이야기이다.
A 클래스가 여러 개 생성되어 A 클래스 안의 B 메소드를 호출해도, 결국 찾아가는 B 메소드의 주소는 모두 같다.
그런데, 이러면 문제가 발생한다. A클래스에서 각자 B메소드를 생성해서 가지고 있다면, B메소드는 자기 자신이 여러 A클래스들 중 어떤 A클래스에 속해 있는지 알 수 있었을 것이다.
하지만, B메소드는 하나만 존재한다. 과연 B메소드는 누가 자신을 호출한 것인지 어떻게 구분할까?
-> 그래서 생성된 모든 클래스는, this라는 포인터 변수가 선언되어 있다.
this는 자기 자신의 주소를 들고 있다.
메소드가 인자를 요구하지 않아도, 클래스는 메소드를 호출할 때 호출한 클래스의 주소(this)를 전달한다.
그렇기 때문에 호출자를 알 수 있는 것이다.
B메소드에 호출한 클래스의 주소 정보를 알려주어, 여러 개의 A 클래스 중 어떤 클래스가 호출했는지 구분을 할 수 있다.
물론, 호출한 객체 주소(this)가 nullptr이여도 메소드가 동작하는 데는 일단 문제가 없으나,
this를 직접 사용하게 된다면 문제가 발생할 수 있으니 주의할 것
class CSharedPtr
포인터 변수를 대신하여 사용할 T 템플릿 클래스를 생성한다.
CF) 템플릿을 사용할 경우 cpp 파일로 선언과 정의를 분리할 수 없으므로 인라인 클래스로 생성한다.
기본적으로 들어온 T타입의 주소를 받을 수 있도록 T타입 포인터 변수를 nullptr로 들고 있다.
다른 클래스에서 접근할 수 없다.
private:
T* m_Ptr;
- 포인터 변수로 사용하게 될 경우 여기저기서 선언될 것이므로 생성과 소멸은 자유로워야 한다.
- 복사 생성자도 만들어 준다.
- 같은 클래스 주소를 받아 복사하거나, 포인터 변수를 받아 복사도 가능하게 한다.
- 이 변수는 생성될 때는 nullptr,
SharedPtr 클래스가 소멸될 때는 주소를 들고 있다면 해당 주소의 Reference Count를 감소시켜주어야 한다.
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();
}
만약 해당 T타입 변수에 새로운 주소가 '신규로' 등록되면, 신규로 등록된 주소를 타고 가서 Reference Count 변수를 하나 올려주어야 한다.
기존에 주소가 있었는데 '변경'되었을 경우, 기존 주소의 Reference Count는 하나 내리고, 신규 주소의 Reference Count를 올려주어야 한다.
포인터 변수를 사용할 때 주로 사용했던 몇몇 연산자를,
위 기준에 맞게 유지하면서 재정의해준다.
* 해당 연산자에 SharedPtr이 들어오던, 기존 포인터 변수가 들어오던, 처리가 가능하도록 오버로딩해준다.
▼힌트
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();
}
}
//위처럼 SharedPtr 객체가 아니라 포인터 변수로 대입연산이 들어올 수 있다.
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;
}
또한 편의성을 위해, T*와 T->, *T 등등의 방식도 아예 SharedPtr로 인식하도록 바꿔준다.
▼참고용 코드
//SharedPtr 클래스를 아예 포인터처럼 사용 가능하게 해준다.
operator T* () const
{
return m_Ptr;
}
T* operator ->() const
{
return m_Ptr;
}
T* operator * () const
{
return m_Ptr;
}
이렇게 해 주면 참조 카운트와 공유 포인터를 사용할 준비는 완료.
여기에 추가로, CRef 클래스가 자식 클래스에 대한 정보도 가지고 있도록 해 주자.
사전 지식의 typeinfo 헤더와 관련된 내용이다.
CRef 클래스에 다음 변수를 선언한다.
- m_Name: string 타입, 프로그래머가 직접 설정해 줄 수 있는 클래스 이름.
- m_TypeID: size_t타입, typeinfo를 통해 해당 객체의 타입을 저장해놓는다.
- m_TypeName: string 타입, typeinfo를 통해 해당 객체의 이름을 문자열로 저장해놓는다.
private:
size_t m_TypeID;
std::string m_TypeName;
std::string m_Name;
void SetType()
CRef 클래스를 어떤 클래스가 상속받을 지 모르기 때문에,
템플릿을 사용하여 초기화를 해 주어야 한다.(생성자를 통한 초기화가 불가능)
그러므로 헤더에 선언과 정의를 동시에 해 준다.
template <typename T>
void SetType()
{
m_TypeName = typeid(T).name();
m_TypeID = typeid(T).hash_code();
}
m_TypeID를 가져오거나,
m_TypeName을 가져오거나,
m_TypeID가 template <typename T>로 들어온 T와 같은지 비교해주는 간단한 함수들을
헤더에 선언 및 정의해준다.
void SetName(const std::string& Name)
{
m_Name = Name;
}
size_t GetTypeID() const
{
return m_TypeID;
}
const std::string& GetTypeName() const
{
return m_TypeName;
}
//T 템를릿으로 들어온 타입과 같은 타입인지 체크
template <typename T>
bool CheckTypeID () const
{
return m_TypeID == typeid(T).hash_code();
}
이제 Ref 클래스와 SharedPtr 클래스에 대한 구현은 끝났다.
이 클래스들을 활용하여 Scene에서 GameObject들을 메모리 누수 없이 관리해 보자.
'WIN32API FrameWork > 한단계씩 직접 구현' 카테고리의 다른 글
15. 참조 카운트로 게임오브젝트 관리 (0) | 2022.05.18 |
---|---|
14. 씬 Scene 구조 짜기 (0) | 2022.05.18 |
12. 삼각함수를 통해 Player에 총을 달아주고 회전/이동시키기 (0) | 2022.05.17 |
11. Player에게 총을 달아주고 입력을 받아 회전시키기 (0) | 2022.05.17 |
10. Vector2를 활용하여 캐릭터를 원하는 위치에 표시하기 (0) | 2022.05.17 |