스마트 포인터란
스마트 포인터란 포인터의 부족했던 기능적인 면들을 보완한 것이다. 스마트 포인터는 shared_ptr, weak_ptr, unique_ptr 세 종류가 있다. 그럼 포인터의 어떤 부족한 면을 보완했는지 알아보자.
shared_ptr
shared_ptr은 여러 포인터 변수가 하나의 객체를 참조하고 있는 상황에서 객체가 delete 되었을 때, 그 객체를 참조하고 있던 포인터 변수들은 이를 모르고 계속 참조하고 있는 상황을 해결하기 위한 스마트 포인터이다.
작동 방식은 기존 포인터를 포인터와 그 포인터가 참조하고 있는 객체를 몇 개의 포인터 변수가 참조하고 있는지 갯수를 가지고 있는 변수를 담은 클래스로 관리하는 것이다. 여기서 중요한 것은 몇 개의 포인터가 참조하고 있는지를 담은 변수를 컨트롤 하는 것이다.
덤으로 shared_ptr을 사용하면 참조하고 있는 포인터가 없으면 자동으로 메모리를 해제해주므로 delete 등을 사용해 직접 해제할 필요가 없어진다.
간단하게 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();
cout << "Constructor RefCount : " << _block->_refCount << endl;
}
}
SharedPtr(const SharedPtr& sptr) : _ptr(sptr._ptr), _block(sptr._block)
{
if (_ptr != nullptr)
{
_block->_refCount++;
cout << "Copy Constructor RefCount : " << _block->_refCount << endl;
}
}
void operator= (const SharedPtr& sptr)
{
_ptr = sptr._ptr;
_block = sptr._block;
if (_ptr != nullptr)
{
_block->_refCount++;
cout << "Copy Operator RefCount : " << _block->_refCount << endl;
}
}
~SharedPtr()
{
if (_ptr != nullptr)
{
_block->_refCount--;
cout << "Destructor RefCount : " << _block->_refCount << endl;
if (_block->_refCount == 0)
{
delete _ptr;
delete _block;
cout << "Delete Data"<< endl;
}
}
}
public :
T* _ptr = nullptr; //참조하고 있는 객체 포인터
RefCountBlock* _block = nullptr;//참조하고 있는 포인터의 갯수 카운트 블록
};
처음 생성했을 때, 복사 생성자를 호출할 때, 복사 대입 연산을 할 때. 세 가지 경우에 대해서만refcount를 증가시켜주면 된다.
포인터가 객체를 더이상 참조하지 않게 되는 상황은 위 클래스의 소멸자가 호출될 때 이다. 이 때만 refcount를 감소시키면 된다.
그럼 이렇게 만들어진 SharedPtr을 어떻게 사용하는지 알아보자.
class Knight
{
public:
Knight() {}
~Knight() {
cout << "Knight 소멸 " << endl;
}
void Attack() {
_target._ptr->_hp--;
cout << "hp : " << _hp << endl;
}
public:
int _hp = 100;
SharedPtr<Knight> _target;
};
int main() {
SharedPtr<Knight> k1 = new Knight();
{
SharedPtr<Knight> k2 = new Knight();
k1._ptr->_target = k2;
} //여기서 k2의 소멸자가 호출됨
k1._ptr->Attack();//정상 작동
}
위 코드에서 처럼 원래 Knight* 로 선언할 것을 shared_ptr<Knight>로 바꿔치기 하듯이 선언하면 된다.
k2가 선언된 블록이 끝나면 k2는 사라져야하지만 k1이 k2를 target으로 참조하고있기 때문에 delete 되지 않는다.
그런데 shared_ptr은 문제가 있다. 그것은 사이클 문제로, 두 포인턴가 서로를 참조하고 있으면 절대 메모리 해제가 안되어서 메모리 누수가 벌어지는 문제이다.
아래 예는 위에서 직접 만든 SharedPtr이 아닌 표준으로 제공하는 shared_ptr을 사용했다.
int main() {
{
shared_ptr<Knight> k1 = makeshared_<Knight>(); //makeshared는 리빙포인트 참조.
shared_ptr<Knight> k2 = makeshared_<Knight>();
k1->_target = k2;
k2->_target = k1;
//k1->target = nullptr;
//k2->target = nullptr;
}
}
위 예를 실행하면 Knight 소멸자 로그가 찍히지 않는 것을 확인할 수 있다. 이렇듯 서로가 서로를 참조하는 shared_ptr은 메모리 해제가 이루어지지 않는다는 것을 볼 수 있다.
주석 처리된 부분을 주석 해제한 뒤 실행하면 Knight의 소멸자가 호출된다.
weak_ptr
위에 설명한 shared_ptr은 reference count를 유지하면서 객체의 수명에 직접적으로 관여한다. 그러나 weak_ptr은 reference count를 증가시키지 않는다. 정확히는 strong reference count를 증가시키지 않고 weak reference count를 증가시킨다. weak reference count는 객체의 수명에 영향을 주지 않는다.
weak_ptr은 expired() 라는 함수를 통해 자신이 가리키는 객체가 유효한지 아닌지 체크할 수 있다.
또한 weak_ptr는 포인터에 직접 접근이 불가능하며, 사용하려면 lock()함수를 통해 shared_ptr로 변환한 뒤에만 사용할 수 있다.
간단히 말하면 reference count는 관여하지 않으면서 참조중인 객체가 잘 살아있는지 확인할 수 있는 포인터라고 할 수 있다.
class Knight
{
public:
Knight() {
cout << "Knight 생성 " << endl;
}
~Knight() {
cout << "Knight 소멸 " << endl;
}
void Attack() {
if (!_target.expired()) {
shared_ptr<Knight> sptr = _target.lock();
sptr->_hp--;
cout << "hp : " << sptr->_hp << endl;
}
}
public:
int _hp = 100;
weak_ptr<Knight> _target;
};
target을 weak_ptr로 바꾸고 Attack()에서 expired와 lock을 사용하고있다.
unique_ptr
unique_ptr은 어떤 객체를 프로그램에서 단 하나의 포인터만 참조하게 하기 위해 만들어진 스마트 포인터이다.
따라서 포인터 복사가 불가능하고, 오로지 이동연산만 가능하다.
unique_ptr<Knight> uptr = make_unique<Knight>();
//unique_ptr<Knight> uptr2 = uptr;//불가능
unique_ptr<Knight> uptr2 = std::move(uptr);//오른값 참조로 바꾸어 이동연산 가능
리빙포인트 1
shared_ptr사용하는 부분에서 shared_ptr<Knihgt> k1(new Knight)를 써도 되지만 shared_ptr<Knihgt> k1 = make_shared<Knight>()를 사용하는 것이 더 성능이 좋다. 또한 메모리 상의 차이점도 있다. make_shared를 사용하면 스마트 포인터의 reference count변수와 weak_reference count 변수를 같은 블록 안에 구겨넣게 된다.
리빙포인트 2
실제 표준 스마트 포인터 클래스들은 공통적으로 상속받는 클래스에 reference count 변수와 weak reference count 변수를 가지고 있다.
'C++' 카테고리의 다른 글
헷갈리는 C++ 질문들 (0) | 2024.04.12 |
---|---|
Casting (Static, Dynamic, Reinterpret ) (0) | 2023.03.28 |
순환문제 (0) | 2022.03.28 |
STL 컨테이너별 간략 특징 정리 (0) | 2022.02.21 |
TLS(Thread Local Storage) (0) | 2022.02.01 |