본문 바로가기

Information Technology/C++

[C++] std::shared_ptr

이 글은 개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;)


이번 포스팅에서는 unique_ptr과 달리, 하나의 주소 값에 대해 여러 변수가 소유하고 관리할 수 있는 std::shared_ptr에 대해 알아보겠습니다.

shared_ptr의 대표적 특징은 다음과 같은 두 가지 입니다.

1. shared_ptr이 선언된 스코프(혹은 블록)가 끝나면 delete 선언을 하지 않아도 스스로 소멸된다.

2. 몇 개의 변수에서 shared_ptr로 할당된 주소 값을 소유하고 다루고 있는지 알려준다.

예제 코드를 통해 shared_ptr의 특징에 대해 더 자세히 알아보겠습니다.

#include <iostream>
#include "Resource.h"
using namespace std;

int main()
{
	Resource* res = new Resource(3);
	res->setAll(1);
	{
		std::shared_ptr<Resource> ptr1(res); // res를 shared_ptr로 저장
		ptr1->print(); // res가 존재하기 때문에 print함수 동작
		{
			std::shared_ptr<Resource> ptr2(ptr1);

			ptr2->setAll(3);
			ptr2->print();

			std::cout << "Going out of the block" << endl;
		}
		ptr1->print();
		std::cout << "Going out of the outer block" << endl;
	}
        std::cout << "the end" << std::endl;
	return 0;
}

3으로 초기화된 Resource 자료형 변수의 주소 값을 동적할당을 통해 변수 res에게 전달해줬습니다.

그리고 res의 배열 멤버를 모두 1로 변경했습니다.

main함수 안에 블록을 만들고, 그 안에서 shared_ptr을 통해 res를 ptr1에 전달해줍니다.

그리고 블록 안에 또 하나의 블록을 생성해서 shared_ptr인 ptr2에 ptr1의 주소값을 전달해줍니다.

이렇게 되면 ptr2는 ptr1에 이어서 두 번째로 res에 대한 소유권을 가지게 되고,

실제로 ptr2에는 자신이 몇 번째로 res를 소유하고 있는지에 대한 정보가 저장된다고 합니다.

 

그렇게 입력한 블록 안에서 ptr2로 ptr1의 setAll 함수와 print 함수를 호출하고,

그 밑에 블록을 빠져나가는 안내문을 출력합니다.

여기서 ptr2가 정의된 블록을 빠져나가면  shared_ptr 인스턴스인 ptr2는 사라집니다. 하지만 ptr1은 ptr2가 정의된 블록보다 상위 블록에서 정의되었기 때문에 소멸되지 않고, 때문에 res 역시 소멸되지 않습니다.

ptr1이 소멸되지 않았기 때문에 첫 번째 블록에서는 여전히 ptr1이 print 함수를 호출할 수 있고,

그 아래에 첫 번째 블록이 끝난다는 안내문을 출력하고 해당 블록을 빠져나옵니다.

이 블록을 빠져나오면서 비로소 shared_ptr 인스턴스인 ptr1이 소멸되고, ptr1에 할당되었던 res 역시 여기서 소멸자를 호출합니다.

이게 예제를 실행한 결과입니다.

마지막에 보이듯이 ptr1이 정의된 블록을 빠져나오면서 res가 소멸자를 호출하고,

main 함수 마지막 코드를 실행하고 프로그램이 종료됩니다.

	std::shared_ptr<Resource> ptr1(res); // res를 shared_ptr로 저장
		ptr1->print(); // res가 존재하기 때문에 print함수 동작
		{
			std::shared_ptr<Resource> ptr2(res);

			ptr2->setAll(3);
			ptr2->print();

			std::cout << "Going out of the block" << endl;
		}
		ptr1->print(); // error!
		std::cout << "Going out of the outer block" << endl;
	}

 주의하실 점이 하나 있는데,

ptr2를 정의할 때, res의 주소 값을 받은 ptr1이 아니라 위의 코드처럼 res의 주소값을 직접 대입하게 되면 ptr2가 선언된 블록이 끝나면서 ptr2가 소멸되고, ptr2에 직접 대입된 res까지 소멸하게 됩니다.

해당 블록이 끝나고 ptr1은 자신이 할당받은 res의 멤버인 print 함수를 호출하는데, res는 이미 소멸되었기 때문에 ptr1은 자신이 호출시킬 멤버를 찾지 못하고 프로그램이 제대로 실행되지 않습니다.

즉, shared_ptr을 통해 하나의 주소 값을 공유할 때에는 shared_ptr 인스턴스를 계속 이어받으면서 주소값을 전달해야 합니다.

auto ptr1 = std::make_shared<Resource>(3);

이런 문제 때문에 실제로 shared_ptr을 사용할 때에는 동적 할당을 통해 독립적으로 존재하는 변수를 shared_ptr에 대입하기보다는,

위의 코드처럼 make_shared를 통해 직접 shared_ptr 인스턴스를 만들어줍니다.

void doSomething(std::shared_ptr<Resource> res)
{}

int main()
{
    doSomething(std::shared_ptr<Resource>(new Resource(100))); // 동적할당으로 인해 문제 발생 가능성
    doSomething(std::make_shared<Resource>(1000));
}

비슷한 이유로, shared_ptr(혹은 unique_ptr 역시 마찬가지로)의 인스턴스를 매개변수로 받는 함수를 호출할 때,

전달 인자로 new를 통해 동적 할당 생성한 인스턴스를 대입하는 것보다는

make_shared를 통해 전달 인자를 할당하는 것이 좋습니다.

{
		auto ptr1 = std::make_shared<Resource>(3);
		ptr1->print();
		{
			auto ptr2 = ptr1;

			ptr2->setAll(3);
			ptr2->print();

			std::cout << "Going out of the block" << endl;
		}
		ptr1->print();
		std::cout << "Going out of the outer block" << endl;
}

make_shared를 통해 shared_ptr의 인스턴스를 만들게 되면, 위의 코드 블록처럼 새로운 shared_ptr 인스턴스를 만들 때에도 편하게 auto를 사용해서 shared_ptr 인스턴스를 할당받으면 됩니다.

 

shared_ptr의 효용이라면, 처음 예제처럼 new를 통해 동적 할당된 인스턴스를 직접 할당받는 경우가 아니라

make_shared를 통해 인스턴스가 생성되었을 때 결국 언젠가 알아서 소멸자를 호출하기 때문에 메모리 누수(memory leak)가 발생할 부담을 덜 수 있다는 점입니다. 메모리 누수의 문제 때문에 share_ptr, 그리고 unique_ptr 역시 필요에 의해 만들어진 게 아닐까 싶습니다.

 

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

[C++] std::string과 std::wstring  (0) 2019.12.07
[C++] 순환 의존성 문제와 weak_ptr  (0) 2019.12.05
[C++] std::unique_ptr(2)  (0) 2019.12.02
[C++] std::unique_ptr(1)  (0) 2019.12.02
[C++] STL의 반복자 iterator  (0) 2019.11.30