C++로 서버나 게임을 개발할 때, 객체의 생명주기를 수동으로 관리해야 하는 상황이 종종 발생합니다. 특히 멀티스레드 환경에서는 객체가 예기치 않게 삭제되어 댕글링 포인터(Dangling Pointer) 문제가 생길 수 있죠.
이런 문제를 해결하기 위해 Reference Counting(참조 카운팅) 기법을 사용합니다. 이는 객체가 몇 군데에서 참조되고 있는지를 추적하고, 참조 수가 0이 되었을 때 안전하게 객체를 삭제하는 방식입니다.
이번 포스트에서는 이를 직접 구현하고, TSharedPtr 이라는 스마트 포인터도 만들어 봅니다.
#pragma once
#include <atomic>
class RefCountable
{
public:
RefCountable() : _refCount(1) {} // 생성 시 기본 참조 1
virtual ~RefCountable() {}
int32 GetRefCount() { return _refCount; }
int32 AddRef() { return ++_refCount; }
int32 ReleaseRef()
{
int32 refCount = --_refCount;
if (refCount == 0)
delete this;
return refCount;
}
protected:
std::atomic<int32> _refCount;
};
AddRef()로 참조 수 증가 ReleaseRef()로 참조 수 감소 delete this로 자신을 삭제class A
{
public:
int v = 10;
};
class B
{
public:
void SetRef(A* a) { _ref = a; }
void DoSomething() { _ref->v += 1; }
private:
A* _ref = nullptr;
};
A* a = new A();
B* b = new B();
b->SetRef(a);
delete a;
while (true)
{
b->DoSomething(); // 위험! 이미 삭제된 메모리를 참조
}
delete a 이후에도 b는 a를 참조함 → 댕글링 포인터template<typename T>
class TSharedPtr
{
public:
TSharedPtr() {}
TSharedPtr(T* ptr) { Set(ptr); }
// 복사 생성자
TSharedPtr(const TSharedPtr& rhs) { Set(rhs._ptr); }
// 이동 생성자
TSharedPtr(TSharedPtr&& rhs) { _ptr = rhs._ptr; rhs._ptr = nullptr; }
// 상속 관계 복사
template<typename U>
TSharedPtr(const TSharedPtr<U>& rhs) { Set(static_cast<T*>(rhs._ptr)); }
~TSharedPtr() { Release(); }
// 복사 연산자
TSharedPtr& operator=(const TSharedPtr& rhs)
{
if (_ptr != rhs._ptr)
{
Release();
Set(rhs._ptr);
}
return *this;
}
// 이동 연산자
TSharedPtr& operator=(TSharedPtr&& rhs)
{
Release();
_ptr = rhs._ptr;
rhs._ptr = nullptr;
return *this;
}
// 편의 연산자
bool operator==(const TSharedPtr& rhs) const { return _ptr == rhs._ptr; }
bool operator!=(const TSharedPtr& rhs) const { return _ptr != rhs._ptr; }
T* operator->() { return _ptr; }
const T* operator->() const { return _ptr; }
T* operator*() { return _ptr; }
const T* operator*() const { return _ptr; }
bool IsNull() { return _ptr == nullptr; }
private:
void Set(T* ptr)
{
_ptr = ptr;
if (_ptr)
_ptr->AddRef();
}
void Release()
{
if (_ptr)
{
_ptr->ReleaseRef();
_ptr = nullptr;
}
}
T* _ptr = nullptr;
};
Set() 시 참조 수를 증가Release() 시 참조 수 감소 후 필요 시 객체 삭제class Wraith : public RefCountable
{
public:
int _hp = 150;
int _posX = 0;
int _posY = 0;
};
using WraithRef = TSharedPtr<Wraith>;
class Missile : public RefCountable
{
public:
void SetTarget(WraithRef target)
{
_target = target;
}
bool Update()
{
if (_target == nullptr)
return true;
// TODO: 추적 로직
if (_target->_hp == 0)
{
_target = nullptr; // 스마트 포인터가 자동으로 소멸 관리
return true;
}
return false;
}
private:
WraithRef _target = nullptr;
};
using MissileRef = TSharedPtr<Missile>;
int main()
{
WraithRef wraith(new Wraith());
wraith->ReleaseRef(); // 초기 2 -> 1로 조정
MissileRef missile(new Missile());
missile->ReleaseRef();
missile->SetTarget(wraith);
wraith->_hp = 0;
wraith = nullptr; // 남은 참조가 missile 하나 뿐
while (true)
{
if (missile && missile->Update())
{
missile = nullptr; // Update 종료 후 자동 소멸
}
}
}
A가 B를 참조하고, B도 A를 참조하면 둘 다 참조 카운트가 0이 되지 않음 → 메모리 릭 발생weak_ptr 또는 약한 참조 개념으로 해결해야 함