VisualStudio/C++

[C++] 스마트 포인터 (shared_ptr, weak_ptr, unique_ptr)

usingsystem 2023. 7. 31. 11:07
728x90

스마트 포인터인

동적 메모리를 할당하면 스택에 주소가 입력되고 힙 메모리에 데이터가 입력되게 된다. 이렇게 동적으로 할당된 메모리는 사용 후 반드시 직접 해제를 해줘야 하는데 더 이상 사용하지 않는 메모리가 메모리를 점유하는 메모리 누수 현상이 일어나기 때문이다. 하지만 직접메모리를 해제하면 메모리관리를 하면서 문제가 발생할 수 있다. 예를 들어 아래와 같이 다른 객체의 포인터를 참조하고 있는 객체가 존재하고 참조하고 있는 객체의  힙 메모리가 해제된 상태로 해당 객체를 접근하게 되면 이미 메모리가 해제된 상태이기 때문에 이미 해제된 메모리에 접근을 하는 문제가 발생하며 해당 오류가 발생하지 않기 위해서는 예외처리도 많이 들어가고 구조를 짜기에도 어려움이 많이 존재한다. 때문에 C++에서는 스파트 포인터는 이런 문제를 방지하기 위해 리소스 누수와 예외 안전성을 보장하는 스마트 포인터기술을 제공하며 즉 포인터를 알맞은 정책에 맞게 관리하는 객체로 포인터를 래핑 해서 사용하는 것이다.  

 스마트 포인터 종류 shared_ptr, weak_ptr, unique_ptr

class Player
{
public:
	Player() {
		cout << "생성" << endl;
	}
	~Player() {
		cout << "제거" << endl;
	}
	void Attack()
	{
		if (_target)
		{
			_target->_hp -= _damage;
			cout << _target->_hp << endl;
		}
	}
public:
	int _hp = 100;
	int _damage = 10;
	Player* _target = nullptr;
};

int main()
{
	Player* p1 = new Player();
	Player* p2 = new Player();

	p1 ->_target = p2;

	delete p2; //동적 메모리 해제.

	p1->Attack();//해제된 메모리 접근

	return 0;
}

shared_ptr

Reference Count(참조 횟수)

shared_ptr은 참조 횟수를 가지고 있다. 즉 해당 포인터를 몇 명이나 참조하고 있는지를 가지고 있다. 그렇기 때문에 delete를 무작정 하는 것이 아니라 참조 횟수가 0이 되었을 때 delete를 하는 특징을 가지고 있다.

생명주기에 직접적으로 관여하기 때문에 순환 구조 영향을 받는다.(서로 다른 포인터 2개가 서로를 참조한 경우 삭제를 할 때 서로 연결이 되어있어 프로그램 종료할 때까지 삭제되지 않는다.)

make_shared

shared_ptr을 추가할 때 make_shared를 하여 추가하면 같은 블록에 저장을 하여 성능상 이점이 있다.

class Player
{
public:
	Player() {
		cout << "생성" << endl;
	}
	~Player() {
		cout << "제거" << endl;
	}
	void Attack()
	{
		if (_target)
		{
			_target->_hp -= _damage;
			cout << _target->_hp << endl;
		}
	}
public:
	int _hp = 100;
	int _damage = 10;
	shared_ptr<Player> _target = nullptr;
};
int main()
{
	shared_ptr<Player> p1 = make_shared< Player>();
	{
		shared_ptr<Player> p2 = make_shared< Player>();
	}
	return 0;
}

Shared_ptr과 this의 관계와 enable_shared_from_this

this는 클래스의 멤버 함수 내에서 객체 자신을 가리키는 포인터입니다.

 

Shared_ptr를 사용하여 객체 자신에 대한 shared_ptr를 생성할 때 객체의 멤버 함수 내에서 shared_from_this()를 사용합니다. 그래서 this를 통한 멤버 함수 내에 자시의 포인터를 사용하면 안 되며 std::enable_shared_from_this를 상속받아야 합니다.

 

std::enable_shared_from_this는 shared_from_this 멤버 함수를 통해 현재 객체의 shared_ptr을 생성할 수 있도록 도와줍니다. 이 클래스는 내부적으로 weak_ptr를 사용하여 객체의 shared_ptr을 관리합니다.

요약

  • shared_ptr는 참조 카운팅을 통해 객체의 생명 주기를 관리합니다.
  • this 키워드는 원시 포인터로, shared_ptr와 직접적인 참조 카운트 관계는 없습니다.
  • std::enable_shared_from_this를 상속받으면, 객체 자신에 대한 shared_ptr를 안전하게 생성할 수 있습니다.
  • shared_from_this를 사용하여 현재 객체에 대한 shared_ptr를 얻을 수 있으며, 이를 통해 참조 카운트를 올바르게 관리할 수 있습니다.
#include <iostream>
#include <memory>

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    std::shared_ptr<MyClass> getSharedPtr() {
   	  std::shared_ptr<MyClass> s = static_pointer_cast<MyClass>(shared_from_this())
        return s;//자기자신의 포인터를 호출
    }

    void show() {
        std::cout << "MyClass::show() called\n";
    }
};

int main() {
    // shared_ptr를 통해 객체를 생성
    std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>();

    std::cout << "Reference count: " << p1.use_count() << "\n"; // 참조 카운트 출력

    // 객체 자신에 대한 shared_ptr를 얻음
    std::shared_ptr<MyClass> p2 = p1->getSharedPtr();
   
    std::cout << "Reference count: " << p1.use_count() << "\n"; // 참조 카운트 출력

    // 두 shared_ptr가 같은 객체를 가리키는지 확인
    if (p1 == p2) {
        std::cout << "p1 and p2 are pointing to the same object\n";
    }

    return 0;
}

Shared_ptr와 람다식

람다 표현식 내부에서 self = shared_from_this()를 사용하는 것은 이 shared_ptr를 캡처하여 람다 함수 내에서 사용할 수 있도록 하는 것입니다. 이렇게 함으로써 람다 함수가 Knight 객체의 수명을 관리하는 shared_ptr를 사용할 수 있습니다.

class Knight : public  enable_shared_from_this<Knight>
{
public:
	void HealMe(int32 value)
	{
		cout << "HealMe! " << value << endl;
	}

	void Test() 
	{
		auto job = [self = shared_from_this()]()
		{
				self->HealMe(self->_hp);
		};
	}

private:
	int32 _hp = 100;
};

 

weak_ptr

shared_ptr과 함께 사용하게 된다면 reference Count 이외에 week Count가 하나 더 추가된다. reference count는  참조 횟수가 0이 되었을 때 delete 하지만 week count가 등장하면 block을 바로 삭제하지 않는다. 또 한 week_ptr은 포인터의 생명주기에 직접적으로 관여를 하지는 않지만 해당 객체가 지워졌는지 안 지워졌는지 알 수 있다. 생명주기에 직접적으로 관여하지 않기 때문에 shared_ptr처럼 순환 구조 영향을 받지 않는다.

class Player
{
public:
	Player() {
		cout << "생성" << endl;
	}
	~Player() {
		cout << "제거" << endl;
	}
	void Attack()
	{
		if (_target.expired() == false)//데이터가 유용하다면
		{
			shared_ptr<Player> sptr = _target.lock();
			sptr->_hp -= _damage;
			cout << sptr->_hp << endl;
		}
	}
public:
	int _hp = 100;
	int _damage = 10;
	weak_ptr<Player> _target;
};
int main()
{
	shared_ptr<Player> p1 = make_shared< Player>();
	shared_ptr<Player> p2 = make_shared< Player>();

	p1->_target = p2;
	p2->_target = p1;
	return 0;
}

unique_ptr

오직 하나만 포인터가 존재할 수 있게 해 줘서 다른 포인터에게 대입할 수 없다. move를 통해 이동만 가능.

즉 그냥 포인터를 사용하는 것 에서 복사기능만 빠져있다고 생각하자.

int main()
{
	unique_ptr<Player> p1 = make_unique< Player>();
	unique_ptr<Player> p2 = std::move(p1);
	//unique_ptr<Player> p2 = p1; 이런식의 복사 안됨

	return 0;
}
728x90