DevNote

스마트 포인터 본문

C++/Modern

스마트 포인터

Ahnda 2020. 2. 13. 21:54

 C 스타일의 프로그래밍의 주요 버그 중 하나는 메모리 누수! 누수는 대개 new를 사용하여 할당된 메모리에 대한 삭제를 호출하지 못한 경우에 발생한다. 최신 C++RAII[1]초기화의 원칙을 강조하는데, 이는 할당된 리소스[2]의 사용 Scope가 끝날 경우에 자동으로 리소스를 해제해주며, 예외 상황에서도 사용한 리소스의 해제를 보장하여 코드의 안정성을 확보하는 Design Pattern이다.


<auto_ptr>

#include <memory>

class A
{
	...
};

int main()
{
	std::auto_ptr<A> temp(new A);	// auto_ptr로 생성하고 초기화
	std::auto_ptr<A> ptr1(new A);	
	std::auto_ptr<A> ptr2(ptr1); 	// 소유권을 ptr1에서 ptr2로 이전
	
	return 0;
}

 메모리 할당 해제를 RAII로 지원하는 auto_ptr이 있습니다. 위의 예제를 보면 사용법은 비교적 간단한 편이다. (auto_ptr을 사용하기 위해선 memory 헤더 추가 필요, 네임 스페이스는 std) 그러나 auto_ptr은 컨테이너에 넣을 수 없는 문제가 있다. 그 이유는 일반적인 복사와는 다르게 동작하기 때문인데, 스마트 포인터 객체를 값으로 전달하면 객체복사가 일어나고, 이 때, 스마트 포인터가 가진 리소스의 복사가 일어나는 것이 아닌 리소스에 대한 소유권을 넘기는 방식이다. 따라서 A 객체에서 B로 복사하면 A가 가지고 있던 리소스가 복사되는 것이 아닌 전달이 되는 것입니다. STL의 알고리즘은 값에 의한 복사가 기본 동작이라서 컨테이너에 auto_ptr을 넣고 알고리즘을 사용하면 리소스의 소유권을 줘버리고 null을 가진 스마트 포인터고 가득 차게 될 것이다. 이를 해결하기 위해 unique_ptr이 나온 것이다.

 

<unique_ptr>

std::unique_ptr<A> ptr1(new A(5));	// 클래스 A 객체형 unique_ptr인 ptr1 선언 및 초기화
auto ptr2 = move(ptr1);			// ptr1에서 ptr2로 소유권 이전

** unique_ptr<A> ptr3 = ptr1;		// 대입 연산자를 이용한 복사는 Error발생

  unique_ptr은 하나의 스마트 포인터만이 특정 개체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터다. 해당 객체의 소유권을 가지고 있을 때만, 소멸자가 해당 객체를 삭제할 수 있다. 또한 unique_ptr 인스턴스는 move() 멤버 함수를 통해 소유권을 이전할 수 있지만, 복사할 수는 없다(복사생성자와 대입연산을 지원하지 않으며, move 생성자 / 대입연산자만 존재). 소유권이 이전되면, 이전 unique_ptr 인스턴스는 더이상 해당 객체를 소유하지 않게 재설정된다.

 

C++14이후로는 make_unique()를 이용하면 unique_ptr 인스턴스를 보다 안전하게 생성할 수 있다. make_unique()는 전달받은 인수를 사용해 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 unique_ptr을 반환한다. 다음은 class A 객체를 가리키는 a라는 unique_ptr을 make_unique() 함수를 이용해 생성하는 예제다.

 

<예제>

#include <iostream>
#include <memory>
using namespace std;

class A
{
private:
	int num;
	int size;
public:
	A(int in_num, int in_size);
	~A() { cout << "소멸자가 호출되었습니다." << endl; }
	void Show();
};

int main()
{
	unique_ptr<A> a = make_unique<A>(1, 28);
	a->Show();

	return 0;
}

A::A(int in_num, int in_size)
{
	num = in_num;
	size = in_size;
	cout << "생성자가 호출되었습니다." << endl;
}

void A::Show()
{
	cout << num << "번 쨰 클래스의 크기는 " << size << "입니다." << endl;
}

<결과>

 위의 예제에서 A 객체를 가리키는 unique_ptr 인스턴스인 a는 일반 포인터와는 다르게 사용이 끝난 후 delete를 사용해 메모리를 해제할 필요가 없이 자동으로 사용을 마친 메모리에 대한 해제를 해주는 것을 확인 할 수 있다.

 

<shared_ptr>

 shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지 참조하는 스마트 포인터다. 이 때, 참조하고 있는 스마트 포인터의 갯수를 참조 횟수(reference count)라고 한다. 참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하며, 수명이 다할 떄마다 1씩 감소한다. 즉, 마지막 shared_ptr의 수명이 다하여, 참조 횟수가 0이 되면 delete를 이용해 메모리를 자동으로 해제한다.

 

<예제>

shared_ptr<A> ptr1(new A(1, 28));	// 클래스 A 타입의 shared_ptr인 ptr1을 선언 및 초기화
cout << ptr1.use_count() << endl;	// 참조 횟수 : 1

auto ptr2(ptr1);					// 복사 생성자를 이용한 초기화
cout << ptr1.use_count() << endl;	// 참조 횟수 : 2

auto ptr3 = ptr1;					// 대입연산자를 이용한 초기화
cout << ptr1.use_count() << endl;	// 참조 횟수 : 3

 위의 예제에서 사용된 use_count() 함수는 shared_ptr 객체가 현재 가리키고 있는 리소스를 참조 중인 소유자의 수를 반환해준다. 또한 make_unique()와 같은 맥락으로 make_shared() 함수는 전달받은 인수를 사용해 지정된 타입의 객체를 안전하게 생성할 수 있다. 

 

 다음의 예제는 A객체를 a라는 shared_ptr를 이용해 make_shared() 함수를 이용해 생성하는 예제이다.

 

<예제>

shared_ptr<A> a = make_shared<A>(1, 28);
cout << "현재 참조 횟수 : " << a.use_count() << endl;

auto b = a;
cout << "현재 참조 횟수 : " << a.use_count() << endl;

b.reset();	// shaed_ptr인 b를 해제함
cout << "현재 참조 횟수 : " << a.use_count() << endl;

<결과>

더보기

생성자가 호출되었습니다.

현재 참조 횟수 : 1

현재 참조 횟수 : 2

현재 참조 횟수 : 1

소멸자가 호출되었습니다.

 위의 예제는 unique_ptr과 마찬가지로 shared_ptr의 인스턴스인 a는 별도의 delete 호출 없이 자동으로 메모리 해제가 되는 것을 보여준다. 또한, 참조 중인 b를 해제했을때, 현재 참조 중인 객체의 갯수가 갱신되는 것을 확인할 수 있다.

 

<weak_ptr>

  weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공한다. 그러나, 소유자의 수에는 포함되지 않기 때문에 shared_ptr의 순환 참조(circular reference)[3]를 방지해줄 수 있다. 즉, weak_ptr은 shared_ptr의 순환 참조를 제거하기 위해 사용된다.

 

 

 

 출처 : https://en.cppreference.com/book/intro/smart_pointers

https://en.cppreference.com/w/cpp/memory/weak_ptr


[1] Resource Acquisition Is Initialization

[2] 대개 메모리와 핸들을 두루 일컫는 말.

[3] 서로가 상대방을 가리키는 shared_ptr이 존재하는 경우, 이 때, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않는다.

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

[C++ 11] final과 override의 차이  (0) 2020.07.17