C++

스마트 포인터

춤추는수달 2022. 3. 29. 03:19

스마트 포인터란

스마트 포인터란 포인터의 부족했던 기능적인 면들을 보완한 것이다. 스마트 포인터는 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