본문 바로가기

C++

스마트 포인터를 이해해보자

똑똑한 포인터


이름에서 알려주다시피 스마트포인터는 일반 포인터보다 똑똑하다. 스마트 포인터는 어떤 점을 개선하기 위해 나온 걸까?

메모리 오염

메모리오염은 할당되지 않는 메모리를 사용하는 것을 말한다. 주로 할당해제를 한 후에도 그 메모리를 가리키는 포인터를 사용할 때 일어난다.

이게 그렇게 심각한 건가?라고 생각이 들 수 있다. 오염시킨 그 공간이 비어있는 상태라면 괜찮지만,

만약 다른 객체들이 할당받았던 상태라면?? 갑자기 다른 객체의 값이 바뀔 수 있는 것이다.

 

이것을 방지하기 위해 똑똑한 스마트 포인터가 개발 됐다.

스마트 포인터의 구동 방식


스마트 포인터는 3가지가 존재한다.

  1. shared_pointer
  2. unique_pointer
  3. weak_pointer

각각 동작 방법은 모두 다르지만, 메모리 오염을 막는 데에 힘쓰고 있다. 이제 각각의 스마트포인터는 어떻게 동작하는지 알아보자.

 

shared_pointer

shared_pointer는 자신을 가리키고 있는 포인터 변수의 수를 카운팅 하여, 카운트가 0이 되기 전까진 메모리를 해제하지 않는다.

shared_pointer는 생성 당시 자기 자신이 참조하고 있으므로 Reference Counting은 1부터 시작한다.

가리키면 1이 증가되고 가리키던 포인터가 없어지면 1이 감소한다.

 

예시 코드

class Family {
public:
	shared_ptr<Family> neighbor;
};

int main() {
	shared_ptr<Family> f1 = make_shared<Family>();
	
	{
		shared_ptr<Family> f2 = make_shared<Family>();
		f1->neighbor = f2;
	}

.....

shared_pointer는 make_shared라는 함수로 생성가능하다.

원래라면 f2는 코드블록의 범위를 나갈 때 해제되어야 하지만, f1이 자신을 reference 하고 있어서 해제되지 않고 대기하게 된다.

주의점

reference counting을 하는 shared_pointer는 lock의 경우와 같이 순환구조가 되는 것을 주의해야 한다.

class Family {
public:
	shared_ptr<Family> neighbor;
};

int main() {
	shared_ptr<Family> f1 = make_shared<Family>();
	shared_ptr<Family> f2 = make_shared<Family>();
	f1->neighbor = f2;
	f2->neighbor = f1;

.....

이 코드를 보면 f1과 f2가 서로를 가리키고 있어 reference count가 절대 0에 도달할 수 없게 됐다.

 

unique_pointer

unique_pointer는 딱 한 포인터만 가리킬 수 있도록 하여 메모리 오염을 방지하는 방법을 사용한다.

다른 포인터가 가리키려고 하면 컴파일 에러가 발생한다.

 

예시 코드

int main() {

	unique_ptr<Family> f1 = make_unique<Family>();

	unique_ptr<Family> f2 = f1; // 컴파일 에러

}

정말 간단한 코드다. unique_pointer는 make_unique라는 함수로 생성할 수 있다.

 

옮기기

unique_pointer에 있는 것을 다른 포인터로 옮기는 법은 정말 없는 걸까?? 당연히도 있다. 오른 값을 이용해 주면 된다.

오른 값을 간단히 설명하자면, 코드의 한 줄을 넘어가면 존재하지 않는, 사용하지 않는 값을 말한다. (ex. 상수) 

오른 값으로 만들어주면 이제 존재하지 않기때문에 다른 변수로 옮겨줄 수 있다.

int main() {
	unique_ptr<Family> f1 = make_unique<Family>();
    
	unique_ptr<Family> f2 = std::move(f1);
}

move함수를 통해 오른값으로 전환할 수 있다.

 

weak_pointer

weak_pointer는 shared_pointer처럼 직접 생명주기에 관여하지 않는다. 또한 직접적으로 포인터를 할당받을 수 없다.

 

이게 무슨 말일까?

weak_pointer는 shared_pointer를 통해 사용할 수 있다. 그것도 shared_pointer의 referenceCounting엔 영향을 주지 않으면서!

 

예시 코드

weak_pointer는 코드를 보며 이해하는 것이 더 효과적이니, 예시 코드를 보며 이해해 보자.

int main() {
	weak_ptr<Family> w_ptr;
    {
		shared_ptr<Family> f1 = make_shared<Family>();
		w_ptr = f1;
	}
    
	if (w_ptr.expired()) {
		cout << "f1 메모리 해제됨";
	}
	else {
		shared_ptr<Family> f = w_ptr.lock();
		// 사용
	}
}

중간에 생성되는 f1 포인터는 코드 블록을 나가면 사라진다. 또한 w_ptr는 shared_pointer인 f1의 포인터를 받아 사용되고 있다. 그것을 생각하며 다음 설명을 보자.

 

현재 위 코드에서 나오는 함수는 2가지이다.

  1. expired()
  2. lock()

expired는 현재 weak_pointer(참조 중인 shared_pointer)가 해제되었다면 true를 반환하는 함수다. 이것으로 weak_pointer가 현재 유효한지 체크할 수 있다.

 

lock함수는 사용 시 shared_pointer를 반환하여 사용한다.

 

정리

weak_pointer를 쉽게 정리하자면 현재 포인터가 존재하는지 체크하고, shared_pointer로 반환하여 사용한다!라고 말할 수 있을 것 같다.

 

마무리


스마트 포인터 3가지는 모두 메모리오염을 막기위해 개발됐지만, 막는 방법이 모두 다르다. 상황에 맞춰 사용한다면 더욱 좋은 프로그램을 제작할 수 있을 것 같다.

'C++' 카테고리의 다른 글

람다(lamda) 표현식을 이해해보자  (6) 2023.10.19