C++ Chapter 15.1 : 이동의 의미와 스마트 포인터

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!


chapter 15. 의미론적 이동과 스마트 포인터

15.1 이동의 의미와 스마트 포인터


🔔 기존의 메모리 관리 방법

1. RAII

RAII 👉 new 로 메모리를 동적 할당 받은 곳에서 return 전에 직접 프로그래머가 delete 해줘야 한다는 디자인 패턴.

  • 보통 클래스 안에 묶어 둠
  • 소멸자들 안에 자원을 해제하는 delete 루틴을 넣는 디자인 패턴
    • 따라서 일반 포인터가 아닌 포인터 객체로 만들어서 자신이 소멸될 때 자신이 가리키는 데이터도 같이 delete 되도록 코딩하는게 일반적이다.
      • 소멸될 때 자동으로 소멸자가 호출되면서 그 소멸자 안에서 delete가 될 수 있도록.

문제가 생기는 경우

  1. 실수로 프로그래머가 delete 을 빼먹은 경우
  2. if-else문 같은 것에 걸려서 delete 되기도 전에 return 되는 경우
  3. try - catch 문에서 delete 되기도 전에 throw 되어 역시 early return 되어버리는 경우
class Resource
{
public:
	int m_data[100];

public:
	Resource()
	{
		cout << "Resource constructed" << endl;
	}

	~Resource()
	{
		cout << "Resource destroyed" << endl;
	}
};

void doSomething()
{
	try 
	{
		Resurce * res = new Resurce;
		
		if (true)
		{
			throw - 1; 
		}		
		delete res; // 📢 if(true)로 인하여 위의 throw에 걸려서 delete이 되지 않는다.
	}
	catch (...)
	{
	}
	return;		
}

int main()
{
	doSomething();
}


2. 메모리를 자동으로 관리해주는 클래스 템플릿 만들기

class Resource
{
public:
	int m_data[100];

public:
	Resource()
	{
		cout << "Resource constructed" << endl;
	}

	~Resource()
	{
		cout << "Resource destroyed" << endl;
	}
};

template<typename T>
class AutoPtr
{
public:
	T* m_ptr = nullptr;

public:
	AutoPtr(T* ptr = nullptr)
		:m_ptr(ptr) {}
	~AutoPtr() 
	{
		if (m_ptr != nullptr) delete m_ptr; //소멸자에서 nullptr 아니면 메모리 지워줌
	}
	T& operator *() const { return *m_ptr; }  // 포인터 연산자 오버로딩을 하여
	T* operator ->() const { return m_ptr; } // 진짜 포인터처럼 작동할 수 있도록
	bool inNull() const { return m_ptr == nullptr; }
};
void doSomething()
{
	try 
	{
		AutoPtr<Resource> res = new Resource;  
		if (true)
		{
			throw - 1; 
		}		
		// delete res; 이제 없어도 됨. 객체 res가 소멸될 때 AutoPtr 클래스의 소멸자에서 알아서 delete 해줄테니까
	}
	catch (...)
	{
	}
	return;		
}

int main()
{
	doSomething();
}
  • 스마트 포인터와 하는 일 비슷하다.
  • Class AutoPtr
    • 클래스 템플릿.
      • 포인터처럼 작동되도록 구현되어 있다.
        • *( ), →( ) 같은 포인터의 간접 참조도 오버로딩 해놓으면 진짜 포인터처럼 쓸 수도 있다.
    • 소멸자에서 nullptr 아니면 메모리 지워주는 일을 한다.
      • 이것만 해줘도 상당히 편함
  • AutoPtr<Resource> res = new Resource;
    • res는 진짜 Resource 타입의 포인터 처럼 사용할 수 있다.
    • 이제 중간에 early return 되더라도 delete 알아서 해준다.
      • AutoPtr 클래스에서 소멸자가 delete 알아서 해주기 때문에

문제가 생기는 경우

int main()
{
	{
		AutoPtr<Resource> res1(new Resource); //-> 초기화 된 상태
		AutoPtr<Resource> res2; //-> 초기화 X nullptr

		cout << res1.m_ptr << endl; // 유효한 주소 출력
		cout << res2.m_ptr << endl; // 00000000 출력 (nullptr)

		res2 = res1;	// 문제 발생 부분 - move semantics

		cout << res1.m_ptr << endl;  // 동일한
		cout << res2.m_ptr << endl;  // 객체를 가리키게 됨
	}
}
💎출력💎

Resource constructed
0014FFA8
00000000
0014FFA8
0014FFA8
Resource destroyed
Resource destroyed
  • AutoPtr<Resource> res1(new Resource);
    • res1은 초기화가 된 상태. (Resource 타입의 특정 객체로)
      • 마치 int i; int *ptr1 = &i; 과 같은 상태다.
  • AutoPtr<Resource> res2;
    • 초기화 ❌ Resource 타입의 어떤 특정 객체의 주소를 아직 안담고 있음. nullptr이다.
      • int * ptr2 = nullptr 와 같은 상태.
  • 대입할 때 문제가 생긴다.
    • 대입할 시 res2 = res1
      • AutoPtr이 가지고 있는 멤버 변수는 포인터 하나.
      • 그 포인터 주소값을 res2 에 복사해준다.
      • res1과 res2가 동일한 Resource 타입의 객체를 가리키게 된다.
        • 둘이 동일한 객체를 가리키게 되므로 블록을 벗어나면서 res1로 접근하여 객체를 소멸 시켰는데 또 res2 에서 이미 소멸된 객체의 메모리를 소멸시키려고 하니까 런타임 에러가 발생하는 것.


🔔 해결 방법 👉 소유권 이동(move semantics)

해결 방법 👉 res2 = res1res1의 객체 소유권을 먼저 박탈하고 난 후에 소유권을 넘겨(Move)주어야 한다.

  • 소유권 이동을 구현할 땐 아래 두가지 중 하나를 손봐야 함
    1. 복사 생성자
    2. 대입 연산자 오버로딩
#include <iostream>
using namespace std;

template<typename T>
class AutoPtr
{
public:
	T* m_ptr= nullptr;

public:
	AutoPtr(T* ptr = nullptr)
		:m_ptr(ptr) {}
	~AutoPtr()
	{
		if(m_ptr != nullptr) delete m_ptr;
	}

	AutoPtr(AutoPtr& a)  // ⭐복사 생성자
	{
		m_ptr = a.m_ptr;
		a.m_ptr = nullptr;  // 소유권 박탈
	}

	AutoPtr& operator = (AutoPtr& a)  // ⭐대입 연산자 오버로딩
	{
		if (&a == this)  // 들어온게 자기 자신이면 아무것도 하지마
			return *this;
		
		delete m_ptr;   // 이미 내가 갖고 있던건 지워버리고
		m_ptr = a.m_ptr;  // 새로 갖다준 주소를 받고
		a.m_ptr = nullptr;   // 갖다주러 들어온 애는 소유권 박탈 시키기 
		return *this;
	}

	T& operator *() const { return *m_ptr; }
	T* operator ->() const { return m_ptr; }
	bool inNull() const { return m_ptr == nullptr; }
};

res2 = res1

  • 대입 연산자 오버로딩
    • 들어 온 것이 자기 자신일 경우(ex. res1 = res1)를 고려해주어야 함
    • 아무것도 하지 않은 채 자기 자신 리턴
    1. 이미 내가 갖고 있던 것은 지워 버리고
      • delete m_ptr포인터인 멤버 delete 시키기
    2. 인수가 새로 갖다준 주소를 받고
      • m_ptr = a.m_ptr
    3. 인수의 소유권은 박탈 시키기
      • a.m_ptr = nullptr nullptr로 초기화.
  • 복사 생성자
    • 나 자신을 새로 생성시킬 때 기존의 다른 객체로부터 복사되어 생성되는 경우 호출 된다.
    1. 인수가 새로 갖다준 주소를 받고
      • m_ptr = a.m_ptr
    2. 인수의 소유권은 박탈 시키기
      • a.m_ptr = nullptr nullptr로 초기화.
      • 대입 연산자 오버로딩가 다르게
      1. 따라서 이미 내가 갖고 있던것을 지울 필요가 없으며
      2. 복사된 것이 자기 자신일 경우도 고려할 필요가 없다.


Semantics Vs. Syntax

  • Syntax 👉 이게 문법에 맞냐 안맞냐, 컴파일이 되느냐 안되느냐.
  • Semantics 👉 컴파일이 되더라도 프로그래머가 의도한 의미대로 굴러 가느냐
    1. value semantics (= copy semantics)
    2. reference semantics (pointer)
    3. move semantics
      • 다음 강의 R-value 에서 더 자세히 다룰 것!


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기