C++ Chapter 15.1 : 이동의 의미와 스마트 포인터
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 15. 의미론적 이동과 스마트 포인터
15.1 이동의 의미와 스마트 포인터
🔔 기존의 메모리 관리 방법
1. RAII
RAII 👉
new
로 메모리를 동적 할당 받은 곳에서 return 전에 직접 프로그래머가delete
해줘야 한다는 디자인 패턴.
- 보통 클래스 안에 묶어 둠
- 소멸자들 안에 자원을 해제하는
delete
루틴을 넣는 디자인 패턴- 따라서 일반 포인터가 아닌 포인터 객체로 만들어서 자신이 소멸될 때 자신이 가리키는 데이터도 같이 delete 되도록 코딩하는게 일반적이다.
- 소멸될 때 자동으로 소멸자가 호출되면서 그 소멸자 안에서
delete
가 될 수 있도록.
- 소멸될 때 자동으로 소멸자가 호출되면서 그 소멸자 안에서
- 따라서 일반 포인터가 아닌 포인터 객체로 만들어서 자신이 소멸될 때 자신이 가리키는 데이터도 같이 delete 되도록 코딩하는게 일반적이다.
문제가 생기는 경우
- 실수로 프로그래머가
delete
을 빼먹은 경우 - if-else문 같은 것에 걸려서
delete
되기도 전에 return 되는 경우 - 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
와 같은 상태.
- 초기화 ❌ Resource 타입의 어떤 특정 객체의 주소를 아직 안담고 있음.
- ⭐대입할 때 문제가 생긴다.
- 대입할 시
res2 = res1
- AutoPtr이 가지고 있는 멤버 변수는 포인터 하나.
- 그 포인터 주소값을 res2 에 복사해준다.
- res1과 res2가 동일한 Resource 타입의 객체를 가리키게 된다.
-
둘이 동일한 객체를 가리키게 되므로 블록을 벗어나면서
res1
로 접근하여 객체를 소멸 시켰는데 또res2
에서 이미 소멸된 객체의 메모리를 소멸시키려고 하니까 런타임 에러가 발생하는 것.
-
- 대입할 시
🔔 해결 방법 👉 소유권 이동(move semantics)
해결 방법 👉
res2 = res1
시res1
의 객체 소유권을 먼저 박탈하고 난 후에 소유권을 넘겨(Move)주어야 한다.
소유권 이동
을 구현할 땐 아래 두가지 중 하나를 손봐야 함- 복사 생성자
- 대입 연산자 오버로딩
#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
)를 고려해주어야 함 - 아무것도 하지 않은 채 자기 자신 리턴
- 이미 내가 갖고 있던 것은 지워 버리고
delete m_ptr
포인터인 멤버 delete 시키기
- 인수가 새로 갖다준 주소를 받고
m_ptr = a.m_ptr
- 인수의 소유권은 박탈 시키기
a.m_ptr = nullptr
nullptr로 초기화.
- 들어 온 것이 자기 자신일 경우(ex.
- 복사 생성자
- 나 자신을 새로 생성시킬 때 기존의 다른 객체로부터 복사되어 생성되는 경우 호출 된다.
- 인수가 새로 갖다준 주소를 받고
m_ptr = a.m_ptr
- 인수의 소유권은 박탈 시키기
a.m_ptr = nullptr
nullptr로 초기화.- 대입 연산자 오버로딩가 다르게
- 따라서 이미 내가 갖고 있던것을 지울 필요가 없으며
- 복사된 것이 자기 자신일 경우도 고려할 필요가 없다.
Semantics Vs. Syntax
- Syntax 👉 이게 문법에 맞냐 안맞냐, 컴파일이 되느냐 안되느냐.
- Semantics 👉 컴파일이 되더라도 프로그래머가 의도한 의미대로 굴러 가느냐
- value semantics (= copy semantics)
- reference semantics (pointer)
- move semantics
- 다음 강의 R-value 에서 더 자세히 다룰 것!
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기