0. 포인터의 문제점
(1) 객체 삭제 이후 더 이상 유효하지 않는 메모리 수정
클래스를 하나 만들고 객체가 다른 객체를 조준하여 공격하는 클래스를 만든다.
class Knight
{
public:
Knight();
~Knight();
void Attack()
{
if (_target)
{
_target->_hp -= _dam;
cout << "상대의 HP : " << _target->_hp;
}
}
public:
int _hp = 200;
int _dam = 10;
Knight* _target = nullptr; // 타겟을 주시함
};
이후에 k1, k2 객체를 동적할당으로 만든다.
int main()
{
Knight* k1 = new Knight();
Knight* k2 = new Knight();
k1->_target = k2; // k1이 k2를 타겟으로 지정
k1->Attack(); // 공격 수행
// 이후 k2 유저가 접속을 종료함.
delete k2;
k1->Attack();
}
여기에서 k2 객체를 삭제하고 다시 k1에게 공격명령을 내리면
k1은 k2의 멤버변수였던 _hp의 더 이상 유효하지 않는(엉뚱한) 메모리 영역을 수정하게 된다.
따라서 _target 포인터를 초기화 시키는 것이 필요한데 이는 번거로운 작업이다.
1. 스마트 포인터
포인터를 알맞은 정책에 따라서 관리한다. 포인터를 래핑해서 사용한다. 그리고 자동으로 메모리를 해제한다.
언리얼 엔진에서는 따로 클래스로 구성하여 사용하고 있다.
(1) 공유 소유권 포인터 ; shared_ptr
여러 개의 포인터가 동일한 메모리 영역을 참조한다.
더 이상 어떤 포인터도 그 객체를 참조하지 않을 때 메모리를 해제한다.
- 원리 구현
class RefCountBlock
{
public:
int _refCount = 1;
};
template<typename T>
class SharedPtr
{
public:
SharedPtr() {}
SharedPtr(T* ptr):_ptr(ptr)
{
if (_ptr != nullptr)
{
_block = new RefCountBlock();
}
}
SharedPtr(const SharedPtr& sptr) : _ptr(sptr._ptr), _block(sptr._block)
{
if (_ptr != nullptr)
{
_block->_refCount++;
}
}
void operator=(const SharedPtr& sptr) // 복사 대입 연산자
{
_ptr = sptr._ptr;
_block = sptr._block;
if (_ptr != nullptr)
{
_block->_refCount++;
}
}
~SharedPtr()
{
if(_ptr != nullptr)
{
_block->_refCount--;
if(_block->_refCount == 0)
{
delete _ptr;
delete _block;
}
}
}
public:
T* ptr = nullptr;
RefCountBlock* _block = nullptr;
};
SharedPtr이 어떤 블록을 가리키게 하고 그 블록을 가리키면 카운트를 하나씩 증가시킨다.
또한 복사 대입 연산자를 만들어서 여러 포인터가 블록을 가리키게 한다.
이후 객체가 소멸될 때마다 소멸자를 통해서 카운트를 감소시키고, 카운트가 0이 되면 포인터와 블록을 삭제한다.
즉, 포인터가 유효하지 않으면 자동으로 메모리를 해제하기 때문에 일일이 직접 메모리를 delete 해 줄 필요가 없다.
- 문법
memory의 헤더 파일을 포함시킨다.
#include <memory>
using namespace std;
class Knight
{
public:
Knight();
~Knight();
void Attack()
{
if (_target)
{
_target->_hp -= _dam;
cout << "상대의 HP : " << _target->_hp;
}
}
public:
int _hp = 200;
int _dam = 10;
shared_ptr<Knight> _target = nullptr; // 타겟을 주시함
};
int main()
{
shared_ptr<Knight> k1 = make_shared<Knight>();
shared_ptr<Knight> k2 = make_shared<Knight>();
k1->_target = k2;
k2->_target = k1;
k1->Attack();
}
그런데 여기에서 k1과 k2가 서로를 참조(순환 참조)할 때 객체가 소멸되지 않는 문제가 있다.
이 이유는 참조카운트가 0이 되지 않기 때문이다.
객체를 소멸하고자 한다면 각 타켓을 nullptr로 만들어주어야 한다.
아니면 weak_ptr를 사용할 수 있다.
(2) 약한 참조 ; weak_ptr
순환 참조가 발생하는 부분을 weak_ptr로 수정한다.
단, 실제로 메모리에 접근하고자 하면 shared_ptr로 변환하여 활용해야 한다.
-문법
class Knight
{
public:
Knight();
~Knight();
void Attack()
{
if (_target.expired() == false)
{
shared_ptr<Knight> sptr = _target.lock();
sptr->_hp -= _dam;
cout << "상대의 HP : " << sptr->_hp;
}
}
public:
int _hp = 200;
int _dam = 10;
weak_ptr<Knight> _target; // 타겟을 주시함
};
int main()
{
shared_ptr<Knight> k1 = make_shared<Knight>();
shared_ptr<Knight> k2 = make_shared<Knight>();
k1->_target = k2;
k2->_target = k1;
k1->Attack();
}
_target.lock()은 shared_ptr을 반환하므로 이를 담는 적절한 변수를 만들어 접근한다.
(3) 고유 소유권 포인터 ; unique_ptr
오직 한 객체만 한 포인터를 소유할 수 있게 함. 다른 포인터가 이 객체를 소유하지 못함.
'기초 C++ 스터디 > 모던 C++' 카테고리의 다른 글
10-9. 람다(lambda) 표현식 (0) | 2023.06.22 |
---|---|
10-8. 전달 참조 (forwarding reference) (0) | 2023.06.22 |
10-7. 오른값 참조(Rvalue Reference) (0) | 2023.06.22 |
10-6. override, final (0) | 2023.06.20 |
10-5. delete - 삭제된 함수 (0) | 2023.06.20 |