Smart Pointer

Min·2024년 7월 14일
0

CPP

목록 보기
11/14
post-thumbnail

Smart Pointer

  • 일반 포인터를 이용한 동적 할당의 문제점
    일반 포인터를 이용한 동적할당은, 프로그래머가 직접 메모리를 추적하고 관리해줘야 하기 때문에, 프로젝트의 규모가 커질 경우 자원 관리에 실패할 가능성이 높아집니다.
    스마트 포인터는 일반 포인터의 이런 문제를 해결하기 위해 등장한 솔루션이라고 할 수 있습니다.

  • 스마트 포인터란?
    스마트 포인터는 동적할당된 메모리 관리를 자동으로 할 수 있도록 디자인된 포인터입니다. 동적할당된 메모리는, RAII 디자인 패턴을 이용하여 객체의 생애주기를 기반으로 메모리를 관리해주며, 객체의 생애주기가 논리적으로 끝났다고 보여질 때, 할당된 메모리를 자동으로 해제해 줍니다.

std::unique_ptr

  • 하나의 오너쉽만 허용하는 포인터
    댕글링 포인터같은 메모리 문제는, 하나의 인스턴스를 여러 포인터가 참조하고 있을 때(공유하고 있을때) 발생하는 문제입니다. 이를 원천 차단 하기 위해서는 애초에 한 인스턴스를 하나의 포인터만 참조할 수 있도록 디자인 하면 됩니다. unique_ptr은 그런 발상으로 부터 디자인된 스마트 포인터라고 할 수 있습니다.

  • 논리적인 생애주기의 끝
    현재 인스턴스를 소유하고있는 단 하나의 unique_ptr 변수가 파괴 된다면, 해당 인스턴스를 참조하는 포인터는 더이상 없기 때문에 메모리를 해제하면 됩니다.

  • unique_ptr의 제한사항
    한개의 오너쉽만 허용하기 때문에, 복사가 불가능합니다. 이동은 당연히 가능합니다.
#include <memory> //스마트 포인터를 사용하기 위한 헤더

int main()
{
	//unique_ptr : 한개의 오너쉽만 허용, 공유가 불가능, 이동은 가능
	{
		std::unique_ptr<int> Uptr(new int); //비효율적 방식의 초기화, make_unique함수를 활용하는 것을 권장
		*Uptr = 10;
		std::unique_ptr<int> Share = nullptr;
		//Share = Uptr; // 복사 및 공유 불가능

		std::unique_ptr<int> Move = nullptr;
		Move = std::move(Uptr); // 이동은 가능
		Uptr.reset(new int); //새로운 메모리에 대한 오너십 세팅
		Uptr = std::make_unique<int>(42); //한가지 방법 더. 이때 위에 reset으로 세팅한 메모리는 소유하고있는 오너쉽이 사라지면서 자동으로 delete됨.
	
		//다른 자료형으로 캐스팅 지원이 안됨
	}

std::shared_ptr

  • 인스턴스의 공유가 가능한 포인터
    앞서 말씀드린 unique_ptr은 안전하지만 너무나 보수적인 방식의 메모리 관리 방법입니다. 포인터의 엄청난 장점 중 하나인 '자원의 공유'를 원천적으로 막아버렸기 때문이죠. std::shared_ptr은 '자원의 공유'를 허용하는 smart pointer입니다. 하나의 인스턴스를 여러 포인터가 소유하고있어도 된다는 뜻입니다.

  • 논리적인 생애주기의 끝
    RAII 디자인패턴으로 해당 리소스를 관리해야 한다면, shared_ptr이 참조하고 있는 인스턴스는 언제 생애주기가 끝나는가에 대해서 고찰해야 합니다. 간단하게 해당 메모리를 소유하고 있는 포인터 변수가 없다면 해당 인스턴스의 생애주기가 끝났다고 말할 수 있습니다. 이를 지속적으로 추적하기 위해서, 인스턴스를 shared_ptr로 참조할 때 마다 Ref Count를 +1, 참조하고 있는 포인터 변수가 없어질때마다 Ref Count를 -1하며 추적합니다. Ref Count가 0이 되면, 더이상 메모리를 소유하고 있는 포인터가 없다는 뜻이기 때문에, 해당 인스턴스를 delete 합니다.

  • std::shared_ptr을 사용할 때의 주의사항
    그러면 shared_ptr은 무적이냐? 하면 그건 또 아닙니다. 잘못 사용하게 되면, Ref Count가 0이 되지 않는 저주에 빠질 수 있습니다. 메모리가 해제되어야 할 때 해제 되지 않는다면 Memory Leak이 남을 수 있다는 뜻이죠. 순환참조가 발생한다면 이런 일이 생길 수 있습니다.
#include <memory> //스마트 포인터를 사용하기 위한 헤더

class Node_CirRef
{
public:
	Node_CirRef()
	{
		std::cout << "Node_CirRef Constructor" << std::endl;
	}

	~Node_CirRef()
	{
		std::cout << "Node_CirRef Destructor" << std::endl;
	}

	std::shared_ptr<Node_CirRef> next = nullptr;
};

class Node : public std::enable_shared_from_this<Node> 
{
public:
	Node()
	{
		std::cout << "Node Constructor" << std::endl;
	}

	virtual ~Node()
	{
		std::cout << "Node Destructor" << std::endl;
	}

	std::weak_ptr<Node> next;
};

class DerievedNode : public Node
{
public:
	DerievedNode()
	{
		std::cout << "DerievedNode Constructor" << std::endl;
	}

	~DerievedNode() override
	{
		std::cout << "DerievedNode Destructor" << std::endl;
	}

	std::shared_ptr<DerievedNode> ReturnThis()
	{
		//return this; 일반적인 방법으로는 불가능함
		return std::dynamic_pointer_cast<DerievedNode>(this->shared_from_this());
	}

	DerievedNode* ReturnRawThis()
	{
		return this;
	}

};

int main()
{
	//shared_ptr : 여러개의 오너쉽을 허용하기 위해서 만들어진 스마트 포인터, 참조 횟수(Ref count)를 계산하는 방식
	{
		std::shared_ptr<int> Sptr = std::make_shared<int>(20); //생성, Ref count 1
		std::shared_ptr<int> Share = nullptr;
		Share = Sptr; //복사 가능, Ref count 2

		std::shared_ptr<int> Move = nullptr;
		Move = std::move(Sptr); // 이동, Ref count 2
		Move.reset(new int(10)); // 리셋, 10->Ref count1 / 20->Ref count1

		const std::shared_ptr<int>& ConstRef = Move; //참조형태로 pass할시, 레퍼런스카운터가 늘지 않음. 10->Ref count1
		std::shared_ptr<int>& Ref = Move; //10->Ref count1
		
		//Ref Count가 0이 되면 메모리 삭제하는 식으로 관리

		//여러 casting을 지원
		std::shared_ptr<Node> node = std::make_shared<DerievedNode>();
		std::shared_ptr<DerievedNode> StaticCast = nullptr;
		std::shared_ptr<DerievedNode> DownCast = nullptr;
		StaticCast = std::static_pointer_cast<DerievedNode>(node);
		DownCast = std::dynamic_pointer_cast<DerievedNode>(node);
		//std::const_pointer_cast<>();

		//주의사항 : 순환 참조
		{
			std::shared_ptr<Node_CirRef> node1 = std::make_shared<Node_CirRef>(); //node1 Ref Count = 1
			std::shared_ptr<Node_CirRef> node2 = std::make_shared<Node_CirRef>(); //node2 Ref Count = 1

			node1->next = node2; //node1 Ref Count = 1, node2 Ref Count = 2
			node2->next = node1; //node1 Ref Count = 2, node2 Ref Count = 2
		}

		// 지역변수 node1 파괴 Ref Count = 1, 지역변수 node2 파괴 Ref Count = 1
		// Ref Count가 0이 아니기 때문에 노드의 소멸자는 호출되지 않음, memory leak
	}
}

std::weak_ptr

  • 순환참조를 해결하기 위한 smart pointer
    앞서 설명한 std::shared_ptr의 가장 큰 문제점은 순환참조였습니다. 프로젝트를 진행하다 보면 의외로 많이 발생하죠. weak_ptr로 참조하는 인스턴스의 Ref Count는 늘어나지 않습니다(대신 weak Ref Count가 증가합니다). 순환참조는 주로 Ref Count를 늘리면 안되는 참조를 할때 발생하는데, 이때 weak_ptr을 사용한다면 문제를 해결할 수 있습니다.
#include <iostream>
#include <memory> //스마트 포인터를 사용하기 위한 헤더
#include <crtdbg.h>

class Node : public std::enable_shared_from_this<Node> 
{
public:
	Node()
	{
		std::cout << "Node Constructor" << std::endl;
	}

	virtual ~Node()
	{
		std::cout << "Node Destructor" << std::endl;
	}

	std::weak_ptr<Node> next;
};

class DerievedNode : public Node
{
public:
	DerievedNode()
	{
		std::cout << "DerievedNode Constructor" << std::endl;
	}

	~DerievedNode() override
	{
		std::cout << "DerievedNode Destructor" << std::endl;
	}

	std::shared_ptr<DerievedNode> ReturnThis()
	{
		//return this; 일반적인 방법으로는 불가능함
		return std::dynamic_pointer_cast<DerievedNode>(this->shared_from_this());
	}

	DerievedNode* ReturnRawThis()
	{
		return this;
	}

};

int main()
{
	//weak_ptr : 약한참조(참조를 통한 접근은 가능, Ref Count를 늘리는 것이 아니라 Weak Ref Count를 늘립니다), 순환참조 문제 방지
	{
		{
			std::shared_ptr<Node> node1 = std::make_shared<Node>(); //node1 Ref Count = 1
			std::shared_ptr<Node> node2 = std::make_shared<Node>(); //node2 Ref Count = 1

			node1->next = node2; //node1 Ref Count = 1, node2 Ref Count = 1
			node2->next = node1; //node1 Ref Count = 1, node2 Ref Count = 1

			bool Check = node1->next.expired(); //오너쉽을 갖고있는 shared_ptr가 생애주기를 끝냈다고 해도 weak_ptr이 파괴되는 것은 아님.
												//만료된 개체를 lock할시 비어있는 shared_ptr을 반환함
			int Count = node1.use_count(); //참조하고있는 shared_ptr의 ref_count를 반환
		}
		//순환참조 문제 해결
	}
}
profile
티내는 청년

0개의 댓글