Mordern C++ 스마트 포인터

CJB_ny·2022년 8월 30일
0

C++ 정리

목록 보기
83/95
post-thumbnail

스마트 포인터 구현 -> 멀티 쓰레드 환경에서 잘 돌아가야함.

아토믹 문법과 활용하게 된다.

왜 필요한가?

포인터 부터 얘기하자.

댕글링 포인터

이까지는 아름답게 동작을 한다.

그런데 main에서 모든 기능들을 다 호출하지 않을 것이다.

이곳 저곳에서 막 사용할 텐데

k2가 접속종료를 함.

이런상황인데 _target자체는 nullptr이 아니기 때문에 크래쉬가 나지 않고 그대로 동작한다.

이런 문제의 원인은 쌩포인터를 사용해서 일어나는 문제이다.

그래서 모든 Knight의 객체의 _target을 nullptr로 밀든가

delete를 호출하지 않던가.

현대적인 c++에서는 스마트 포인터를 간접적으로 활용한다.

언리얼만 봐도 스마트 포인터 사용한다.

즉, 효율보다 코드의 안정성이 훨씬 더 중요하기 때문에

프로그램 생명주기를 포인터가 아니라 스마트 포인터를 사용해야한다.

스마트 포인터란?

포인터를 알맞는 정책에 따라 관리하는 객체 (포인터를 매핑해서 사용)

여기서 '매핑'했다라는 것은 포인터를 그대로 사용하는게 아니라 다른 아이를 만들어서 해당 클래스 객체를 삭제할지 안할지 정하는 결정하는 정책이 있다.

스마트 포인터 알아야 할점

  • shared_ptr : 주인공 (핵심), 맏형

  • weak_ptr : 주인공 단점 매꾸는애

  • unique_ptr

이거 세가지에 대해서 알아야한다.

이 세가지 개념은 언리얼 엔진에서도 똑같이 들어가있다.

언리얼에서는 표준 shared, weak, unique를 사용하지 않고 (기능은 똑같은데) 다른 이름으로 사용한다.

이 세가짐 개념은 어디서나 유효하다.

shared_ptr

포인터를 관리를 하기는 하는데

레퍼런스 카운트라는 것을 관리할 것이다. 말그대로 참조 카운트.

이러한 포인터 자체를 몇명이나 참조를 하고 있는지를 관리를 한다.(추적을 해가지고)

멋대로 delete k2;하는게 아니라

반드시 k2라는 객체를 아무도 기억하지 않을 때, 그때 비로소 delete를 한다는 특징이 있다.

구현방법

구현방법은 여러가지가 있다.

가장 기본적인 방법만 사용해서 간단하게 구현 해보도록 하자.

그다음 ref카운트를 세야하는데

보통 일반적으로 따로 빼서 공용메모리로 관리하는게 일반적이다.

dl _refCount가 0이되면 아무도 기억을 못하는 상태라고 본다.

class RefCountBlock
{
public :
	int _refCount = 1;
};

template <typename T>
class SharedPtr
{
public :
	SharedPtr() { } // 이대로 놔두면 nullptr로 아무것도 안하는 존재가 된다.
	SharedPtr(T* ptr)
		:
		_ptr(ptr),
	{
		if (_ptr != nullptr)
		{
			_block = new RefCountBlock();
			cout << "RefCount : " << _block->_refCount << endl;
		}
	}
public :
	T* _ptr;
	RefCountBlock* _block;
};

이렇게하면 생명주기가 따라간다고 보면은 된다.

(이거 static으로 안들고 있어도 되는 이유가 SharedPtr하나만 있을 거고 그 하나의 객체안에 멤버 포인터 변수로 힙을 가르키는 주소를 들고 있을 것이기 때문에)

그런데 SharedPtr객체를 삭제하면 어떻게 되나??

	~SharedPtr()
	{
		if (_ptr != nullptr)
		{
			_block -> --_refCount;
			cout << "RefCount : " << _block->_refCount << endl;

			if (_block->_refCount == 0)
			{
				delete _ptr;
				delete _block;
				cout << "Delete Data" << endl;
			}
		}
	}

소멸자는 이렇게 만들어 주도록 하자.

사용

{} 블록을 벗어나면 소멸자를 호출하게 된다.

이까지는 생포인터와 다를게 없다.

다른점

이렇게 복사를 받는 경우

복사 생성자를 구현을 해주도록 한다.

근데 여기서 끝나는게 아니다.

nullptr이 아니라면 하나 증가 시켜 주어야한다.

이렇게 되면

k1, k2둘다 같은 객체를 참조를 하고 있는 형태이다.

설령 k1이 삭제가 된다고 하더라도, k2에 의해서 당장은 new Knight()라는 객체가 사라지지는 않는다.

지금 이렇게하면 k1을 k2에다가 복사 대입 연산자를 하는 중이다.

class Knight
{
public:
	Knight()
	{}

public:
	int _id = 0;
	int _damage = 10;
	int _defense = 20;

};

class RefCountBlock
{
public :
	int _refCount = 1;
};

template <typename T>
class SharedPtr
{
public :
	SharedPtr() { } // 이대로 놔두면 nullptr로 아무것도 안하는 존재가 된다.
	SharedPtr(T* ptr)
		:
		_ptr(ptr)
	{
		if (_ptr != nullptr)
		{
			_block = new RefCountBlock();
			cout << "RefCount : " << _block->_refCount << endl;
		}
	}

	SharedPtr(const SharedPtr& sptr)
		:
		_ptr(sptr._ptr),
		_block(sptr._block)
	{
		if (_ptr != nullptr)
		{
			_block->_refCount++;
			cout << "RecCout : " << _block->_refCount << endl;
		}
	}
	void operator = (const SharedPtr& sptr)
	{
		_ptr = sptr._ptr;
		_block = sptr._block;

		if (_ptr != nullptr)
		{
			_block->_refCount++;
			cout << "RecCount : " << _block->_refCount << endl;
		}
	}

	~SharedPtr()
	{
		if (_ptr != nullptr)
		{
			_block->_refCount--;
			cout << "RefCount : " << _block->_refCount << endl;

			if (_block->_refCount == 0)
			{
				delete _ptr;
				delete _block;
				cout << "Delete Data" << endl;
			}
		}
	}
public :
	T* _ptr;
	RefCountBlock* _block;
};


int main()
{
	SharedPtr<Knight> k2;

	{
		SharedPtr<Knight> k1(new Knight());
		k2 = k1;
	}

	int a = 10;

	return 0;
}

이렇게 스마트 포인터를 사용하는 순간

명시적으로 기사를 delete할 필요가 없고 알아서 delete를 해준다.

(C#처럼) -> 메모리 관리에서 벗어나게된다.

그래서 이제 표준 shared_ptr을 사용해보도록 하겠다.

표준 shared_ptr 사용

이렇게 되어있던 부분을 이제는

이렇게 사용하면된다. (비슷한 방식으로 구현되어있다)

이렇게 여기다가 아까처럼 new Knight를 넣어주어도 상관은 없는데

make_shared< Knight > (); 로 해주는게 성능에 조금 더 좋다.
(메모리 블럭을 한번에 만들어 준다는 차이점이 있다)

k2도 똑같이 만들어주고, k1의 _target이 이제 스마트 포인터로 주시를 하게 된다는 것이다.

다시

이런상황이라 가정을 하자. k2는 {} 안에서만 유효하다.

그럼에도 불구하고 k2는 _target이 기억하고 있으니까,


여기서 만들어준 make_shared로 만들어준 Knight객체는 당장은 삭제가 안된다.

Attack을 실행해도 문제없이 hp가 깍인다.

이렇게하면

'Use After Free' 문제 '댕글링'문제를 해결할 수 있다.

모든 C++11에서 부터는 쌩포인터 안 사용한다고 보면은 된다.

궁금한점

use after free, 댕글링 문제 스마트 포인터를 통해 메모리 오염 막을 수 있는거 알겠다.

그런데 지금 k2가 {} 지나면 유효하지 않은데 굳이 또 k1->Attack()을 호출할 필요가 있나?

k2의 데이터자체는 사용할 수 없을 텐데...

weak_ptr

shared_ptr이랑 셋트로 보면 된다.

shared_ptr은 아무도 자신을 기억하지 않을 때 소멸시킨다는 장점은 있다.

그런데 약간 문제가 있는게 뭐냐하면은

전통적으로 이 '사이클'문제를 해결할 수 없다.

사이클 문제?

현재 서로가 서로를 주시를 하는 상황이라고 가정을 해보도록 하자.

메모리를 그려보면은 서로 처음 만들어 졌을때 _refCount값이 1, 1인 상태이다.

k1->_target = k2; 를 하게되면 k1의 _refCount 1증가함.
k2->_target = k1; ''

이렇게 서로를 주시를 하고있기 때문에

_refCount가 1씩 증가를 하게 될 것이다.


아까 우리가 직접만든 SharedPtr클래스를 보면 _target = k2넣으면

복사 대입 연산자가 발생해서 _refCount를 ++해준다.


그러면 서로의 _refCount가 2가 되는 것 까지는 알겠다.

그러면 이제

여기서 {}지나면 shared_ptr의 객체 k2가 소멸이 되면서

k2 의 _refCount가 1이 줄어들게된다.

여기서 이제 끝나는게 다다.

k2가 소멸이 안되는 이유는 k1에서 k2를 주시를 하고 있기 때문에

k2의 _refCount값이 1이하로는 절대로 떨어지지 않는다는 문제점이 있다.

서로가 서로를 주시를 하고있으니까

어떠한 경우에도 절대로 메모리가 소멸이 되지 않는다.

프로그램 끝나도 소멸자 호출을 안한다.

따라서 shared_ptr만 사용한다면 이 '사이클'문제를 주의 해야한다.

정 이것을 해결 하고싶다면은

이런식으로nullptr로 밀어주어야한다.

이런 shared_ptr을 사용할 떄

'생명주기'문제에 앞서서 '순환구조'가 발생하지 않는지 봐야한다.

weak_ptr

이런 '순환 구조'가 일어날 수 있는 부분을

'weak_ptr'로 바꿔주면된다.

지금 30번째줄에서 shared_ptr에서 순환 구조가 일어나는 것인데

이부분을 이제

이부분을 이렇게 바꿔주면된다.

우리는 RefCountBlock에서 _refCount만 세는중이였는데 이제는

이런식으로 weakCount가 더 추가 됬다고 보면은 된다.

_refCount는 진짜 이 객체를 참고를 하고있는 애가 몇명인지 카운트 하는 것이고

_weakCount의 경우에는 몇명(몇개)가 RefCountBlock을 참고를 하는지 나타낸다.

우리가 소멸자 부분에서 _refCount가 0이되면 _block까지 같이 delete 날려줬었는데

weak_ptr이 있으면 이 블록을 당장은 삭제하지 않고 살려두게된다.

그래서 weak_ptr을 통해서 해당 메모리가 날라갔는지 안 날라갔는지 '확인'하는 용도로 사용할 수 있다.

shared_ptr처럼 그 객체의 생명주기에 직접적으로 관여하는 녀석은 아니지만, 메모리가 날라갔는지 안 날라갔는지 확인하는 용도로 사용한다고면은 된다.

그래서 포인터처럼 막바로 사용할 수는 없다.

이렇게 expired()라는 함수로 _target 이라는 포인터가 메모리에서 날라갔냐 안날라갔냐를 유효한지 안한지를 체크를 해주어야한다.

false라면은 아직 유효하다는 말이다.

유효하다면 lock()을 사용한다(shared_ptr을 뱉어준다)

이렇게 shared_ptr로 받아서 사용하면된다.

두단계에 걸쳐서 일어나게된다.

이렇게 단점으로는 확인하고 사용하는 두단계로 걸쳐서 사용한다는 귀찮은 점이고

장점으로는 메모리 구조에서 좀 자유로워지고...

shared_ptr, weak_ptr

어떤 포인터를 매핑해가지고 '생명주기'를 자동으로 관리를 하는 애라고 기억을 하자.

count설명

지금 _target이 weak_ptr로 잡혀있기 때문에

_recCount는 세어지지 않는다.

그래서 return 0;을 만날때 소멸이 되면서

_refCount가


0, 0이 되면서 메모리가 날라간다고 보면은 된다.

지금 {} 안에서 나오면은 k2가 유효하지 않게 되는데

이럴경우 실행을 하면은

Attack에 들어가서 보아도 expired를 체크를 해서 유효하지 않다(true)가 나오기 때문에 if문이 실행되지 않는다.

이상태라면은 true가 나와서 if문을 실행을 한다.

Unique_ptr

세상에서 딱 하나만 존재를 하는 포인터 이다.

정말 넘겨주고 싶다면은

오른값으로 넘겨주어야 가능하다.

일반적은 복사는 막히고 '이동'만 가능하다.

쌩포인터와 비슷한데 복사만 막혀있다고 보면된다.

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글