728x90

Unique Pointer에 이어서 Smart Pointer의 한 종류인 Shared Pointer에 대해서 정리한다. 

 

 

Shared Pointer (쉐어드 포인터)

 

이름에서도 알 수 있듯이 해당 포인터는 Exclusive Ownership을 가지는 Unique pointer와 다르게 Shared Ownership을 가지게 된다. 그 말은 하나의 오브젝트를 여러 개의 포인터가 가질 수 있다. 하지만 모든 스마트 포인터는 RAII콘셉트를 제공을 해야 한다. Shared Pointer는 여러 개의 포인터가 가리킴에도 불구하고 어떻게 오브젝트의 메모리 할당을 해제시키는지 알아보자. 

 

아래 코드는 쉐어드 포인터를 선언하는 코드이다. 

class Dog
{
public:
    Dog() : mAge { 0 }
    {
        std::cout << "dog Constructor" << std::endl;
    }
    ~Dog()
    {
        std::cout << "Dog destructor" << std::endl;
    }
private:
    int mAge;
}

int main()
{
    std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
}

위 코드에서 dogPtr이라는 쉐어드 포인터를 생성하고 Dog객체를 할당하였다. 위 과정에서 해당 포인터의 scope단위는 main함수이고 객체가 생성이 되면서 Constructor를 부르게 되고 main함수가 종료되면서 자동적으로 Destrucotr를 호출하게 된다. 

 

하지만 쉐어드 포인터의 특성으로 다른 쉐어드 포인터가 하나의 오브젝트를 가리킬 수 있다. 아래의 코드는 여러 개의 쉐어드 포인터가 하나의 오브젝트를 가리키는 예이다. 

int main()
{
    std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
    std::shared_ptr<Dog> dogPtr1 = dogPtr;
}

위와 같은 경우 Unique Pointer와 다르게 하나의 오브젝트를 두 개의 포인터가 가리키게 된다. 만약에 여기서 끝이라면 일반 포인터와 다를 바가 없다. 일반 포인터와 다른 점은 가리키는 메모리 공간에 몇 개의 포인터가 오브젝트를 가리키는지 Count를 한다. Count 중에서도 Strong Count와 Weak Count가 있는데 쉐어드 포인터가 가리키게 되면 Strong Count가 올라가고 오브젝트는 Strong Count가 0일 때 메모리를 해제를 결정하게 된다. Weak Pointer가 가리키게 되는 경우는 Weak Count가 올라가지만 메모리를 해제하기 위해서 참고되지는 않는다.

 

Shared Pointer은 개발자가 Rsource의 Life Cycle을 고려하지 않고(메모리 해제를 고려하지 않고) 한 오브젝트의 Ownership을 여러 Scope에서 공유가 가능하게 만들어준다. 

 

하지만 이러한 특성 때문에 의도치 않은 Memory Leak이 일어날 수 있다

 

아래 코드는 Shared Pointer 사용함에 있어서 Memory Leak이 일어나는 가장 쉬운 예이다. 

 

#include <iostream>
#include <memory>

class Cat
{
public:
	Cat(std::string name) : mName{ name }
	{
		std::cout << mName << " cat constructor" << std::endl;
	}
	~Cat()
	{
		std::cout << mName << " cat destructor" << std::endl;
	}
	std::shared_ptr<Cat> mVar;
    
private:
	std::string mName;
};

int main()
{
	std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
	kitty->mVar = kitty;
	std::cout << kitty.use_count() << std::endl;
}

먼저 클래스 내에 Shared Pointer mVar를 추가로 생성하였다. main함수에서 Shared Pointer kitty를 선언하고 오브젝트를 생성하고 오브젝트 내의 Shared Pointer mVar를 오브젝트 자신인 kitty를 가리키게 만들었다. 그다음 kitty 오브젝트의 Strong Count를 확인하기 위해 use_count()를 이용해 출력을 한 결과 2가 출력이 된다. 이 경우  Memory Leak이 발생하게 되는데 아래 그림을 통해 알아보자.

 

Shared Pointer인 kitty는 main함수 scope가 종료됨에 따라 메모리 해제가 된다. kitty가 사라지고 오브젝트의 Count가 2에서 1로 줄게 된다. Shared Pointer가 해제되고 오브젝트 또한 자동으로 해제되는 것을 기대했지만 오브젝트 내부의 Shared Pointer가 오브젝트를 아직 가리키고 있기 때문에 main함수가 종료됐음에도 불구하고 메모리 해제가 안되어 Memory Leak이 일어난 경우이다. 

 

위 예는 Memory Leak이 일어날 수 있다는 것을 보여주기 위한 예로 실제 많이 발생되지 않는다. 실제로 자주 발생되는 Memory Leak에 대한 코드는 아래와 같다. 


Circular Reference (순환 참조)

 

#include <iostream>
#include <memory>

class Cat
{
public:
	Cat(std::string name) : mName{ name }
	{
		std::cout << mName << " cat constructor" << std::endl;
	}
	~Cat()
	{
		std::cout << mName << " cat destructor" << std::endl;
	}
	std::shared_ptr<Cat> mVar;
private:
	std::string mName;
};

int main()
{
	std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
	std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");

	kitty->mVar = nabi;
	nabi->mVar = kitty;

	std::cout << "kitty count : " << kitty.use_count() << std::endl;
	std::cout << "nabi count : " << nabi.use_count() << std::endl;
}

 

순환 참조란 위 코드와 그림처럼 클래스 내부에서 Shared Pointer로 다른 클래스가 서로 가리키는 것을 의미한다.  kitty 오브젝트와 nabi 오브젝트가 Shared Pointer kitty와 nabi에 의해서 가리키게 되고 내부의 mVar Shared Pointer가 서로의 오브젝트를 가리키게 되면 Shared Pointer가 main함수에서 메모리 해제가 되어도 각 오브젝트의 Count는 1로 메모리 해제가 안 되는 것을 말한다. 

 

위와 같은 Memory Leak을 해결하기 위해 Weak_ptr을 이용하여 Circular Reference를 구현할 수 있다. 다음 포스팅은 Weak_Ptr에 대해서 공부할 예정이다. 

 

 

Ref.

https://www.youtube.com/watch?v=tg34hwP0P0M&list=PLDV-cCQnUlIbOBiPvBaRPezOLArubgZbQ&index=4 

+ Recent posts