C++ Chapter 15.5 : 스마트 포인터1️⃣ std::unique_ptr
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 15. 의미론적 이동과 스마트 포인터
15.5 스마트 포인터1️⃣ std::unique_ptr
🔔 스마트 포인터란?
#include <memory.h> 해주어야 사용 가능하다.
- 스마트 포인터
- 👉 포인터가 참조하고 있는 동적 메모리를 자동으로 delete 시켜준다. scope를 벗어나면 알아서 소멸자를 호출해주기 때문!
- 모든 스마트 포인터는 공통적으로 이 특징을 가짐.
- 프로그래머가 직접
delete
를 명시해줄 필요가 없다.- 메모리 누수를 방지해준다.
- if-else문에 만나거나 예외를 만나 throw 되는 등등 일찍 return 되어
delete
문을 만나지 못하는 경우를 방지함
- if-else문에 만나거나 예외를 만나 throw 되는 등등 일찍 return 되어
- 메모리 누수를 방지해준다.
->
,*
연산도 오버로딩 되어 있기 때문에 일반 포인터처럼 사용이 가능하다.
- 👉 포인터가 참조하고 있는 동적 메모리를 자동으로 delete 시켜준다. scope를 벗어나면 알아서 소멸자를 호출해주기 때문!
- 스마트 포인터의 종류
- unique_ptr
- shared_ptr
- weak_ptr
- 선언할 때 일반 포인터처럼
*
을 붙이지 않는다.Resource * res = new Resource(1000000); // 일반 포인터 선언과 정의 std::unique_ptr<Resource> res(new Resource(1000000)); // 스마트 포인터 선언과 정의
🔔 unique_ptr
- 특정 객체에 유일한 소유권(unique)을 부여하는 포인터 객체
- 포인터가 가리키고 있는 데이터의 소유권이 한 곳에만 속할 경우 사용하는 스마트 포인터.
- 이 객체를 잘 보관하고 막아주겠다는 성격이 강함
#include <iostream>
#include <memory> // ⭐⭐
using namespace std;
class Resource
{
public:
int * m_data = nullptr;
unsigned m_length = 0;
public:
Resource()
{
cout << "Resource constructed" << endl;
}
Resource(unsigned length)
{
cout << "Resource length constructed" << endl;
this->m_data = new int[length];
this->m_length = length;
}
Resource(const Resource &res) // 복사 생성자
{
cout << "Resource copy constructed" << endl;
Resource(res.m_length);
for (unsigned i = 0; i < m_length; ++i) // 깊은 복사
m_data[i] = res.m_data[i];
}
~Resource() // 소멸자
{
cout << "Resource destroyed" << endl;
}
Resource & operator = (Resource & res) // 대입 연산자 오버로딩
{
cout << "Resource copy assignment" << endl;
if (&res == this) return *this;
if (this->m_data != nullptr) delete[] m_data;
m_length = res.m_length;
m_data = new int[m_length];
for (unsigned i = 0; i < m_length; ++i)
m_data[i] = res.m_data[i]; // 깊은 복사
return *this;
}
void print() // Resource의 동적 배열 m_data의 모든 원소값을 출력한다.
{
for (unsigned i = 0; i < m_length; ++i)
std::cout << m_data[i] << " ";
std::cout << std::endl;
}
void setAll(const int& v) // Resource의 동적 배열 m_data 의 모든 원소값을 v 값으로 설정한다.
{
for (unsigned i = 0; i < m_length; ++i)
m_data[i] = v;
}
};
int main()
{
{
std::unique_ptr<int> upi(new int); // ⭐int 인스턴스를 가리키는 스마트 포인터 upi
auto *ptr = new Resource(5);
std::unique_ptr<Resource> res1(ptr); // ⭐Resource 타입의 인스턴스를 가리키는 스마트 포인터 res1
res1->setAll(5); // 모든 원소를 5 로 설정
res1->print(); // 모든 원소 출력
}
}
💎출력💎
Resource length constructed
5 5 5 5 5
Resource destroyed
unique_ptr의 함수들
make_unique 함수
std::make_unique
함수 👉 unique_ptr 인스턴스를 안전하게 생성할 수 있다.
- 전달 받은 인수를 사용하여 지정된 타입의 객체를 생성
- 생성된 객체를 가리키는
unique_ptr
을 리턴받을 수 있다.- std::unique_ptr<Resource> res1 = new Resource(5) 는 리턴 없이 단순히
unique_ptr
를 선언하는 것이 되는 반면에std::make_unique
함수를 사용하면unique_ptr
를 리턴 받을 수 있다.
- std::unique_ptr<Resource> res1 = new Resource(5) 는 리턴 없이 단순히
int main()
{
{
std::unique_ptr<int> upi(new int);
auto res1 = std::make_unique<Resource>(5);
// std::unique_ptr<Resource> res1 = new Resource(5);
res1->setAll(5);
res1->printf();
}
}
auto res1 = std::make_unique<Resource>(5)
- new Resource(5) 하여 생성자를 호출하고 객체를 생성한 후
- 이 객체를 가리키는 unique_ptr를 리턴하여
res1
에 복사된다. 👉 얕은 복사
get 함수
get()
unique_ptr 자체에서 가지고 있는 함수로 일반 포인터를 리턴시킨다.
void doSomething2(Resource * res) // 일반포인터
{
res->setAll(100);
res->print();
}
int main()
{
auto res1 = std::make_unique<Resource>(5);
doSomething2(res1.get());
}
res1.get()
- unique_ptr인
res1
을 일반 포인터로서 리턴하여 doSomething2 함수에게 인수로 넘기고 있다.
- unique_ptr인
std::unique_ptr의 특징
// res1, res2는 unique_ptr 타입의 스마트 포인터
auto res1 = std::make_unique<Resource>(5);
std::unique_ptr<Resource> res2;
res2 = res1; // ❌오류! unique_ptr은 복사를 못한다.
res2 = std::move(res1); // ⭕
- 복사를 못 한다. Copy Semantics는 안됨. 컴파일 오류 남!
unique_ptr
은 ✨한 객체의 소유권은 오로지 한 곳에서만 가질 수 있기 때문에res2 = res1
이렇게 포인터끼리 단순히 복사를 하면res2
와res1
두 곳에서 동일한 인스턴스에 대해 소유권을 가지게 되기 때문에!
- 이동만 할 수 있다. Move Semantics만 사용 가능.
res2 = std::move(res1)
res1
이 R-value로 바뀐다.res1
은 소유권이 박탈 되어 이제 아무 객체도 가리키지 않는 nullptr이 되고res2
은res1
이 소유하고 있던 객체의 소유권을 물려 받게 된다.
res1
은 소유권이 박탈되고 그 소유권이res2
로 이전되므로 한 객체의 소유권이 오로지 한 곳에서만 가질 수 있다는 unique_ptr의 성질이 보장된다.
이처럼
unique_ptr
은 서로의 단순 복사를 막아 어떤 객체에 대한 소유권을 오로지 하나의unique_ptr
에서만 가리킬 수 있도록 보장해준다.
auto doSomething()
{
return std::unique_ptr<Resource>(new Resource(5));
// return std::make_unique<Resource>(5);
}
int main()
{
{
auto res1 = std::make_unique<Resource>(5);
// auto res1 = doSomething();
res1->setAll(5);
res1->print();
std::unique_ptr<Resource> res2; // unique_ptr인 res2는 선언만 됐고 아직 가리키고 있는 객체는 없으므로 nullptr인 상태
// std::boolalpha : 0, 1 대신 true, false 출력. null이면 false출력.
std::cout << std::boolalpha;
std::cout << static_cast<bool>(res1) << std::endl; // true 출력
std::cout << static_cast<bool>(res2) << std::endl; // false 출력
res2 = std::move(res1);
std::cout << std::boolalpha;
std::cout << static_cast<bool>(res1) << std::endl; // false 출력. res1은 소유권을 잃어 nullptr이 됨
std::cout << static_cast<bool>(res2) << std::endl; // true 출력. res2에게로 소유권이 이전됨
if (res1 != nullptr) res1->print();
if (res2 != nullptr) res2->print();
}
}
💎출력💎
Resource length constructed
5 5 5 5 5
true
false
false
true
5 5 5 5 5
- 함수 리턴 값은 R-value이니까
unique_ptr
를 리턴하는doSomething()
은unique_ptr
를 R-value로서 리턴하는 것이나 마찬가지.- auto res1 = doSomething();
- 임시 객체 리턴의 소유권이
res1
로 이전
- 임시 객체 리턴의 소유권이
- auto res1 = doSomething();
- auto res1 = std::make_unique<Resource>(5);
res1
은 현재 Resource 객체를 가리키고 있는 중
- std::unique_ptr<Resource> res2;
- unique_ptr인
res2
는 선언만 됐고 아직 가리키고 있는 객체는 없으므로 nullptr인 상태- 정의까지 하려면 std::unique_ptr<Resource> res2 = new Resource(5); 가 됐었어야 함
- unique_ptr인
- res2 = std::move(res1);
res1
이 가리키던 객체의 소유권이res2
에게로 이전 되었기 때문에res1
은 소유권이 박탈 되어 nullptr이 된다.
Resource * res = new Resource;
std::unique_ptr<Resource> res1(res);
std::unique_ptr<Resource> res2(res); // ❌에러
delete res; // ❌불상사 발생 가능성
- 일반 포인터
res
가 가리키고 있는 객체를 unique_ptr인res1
과res2
가 동시에 소유하려고 하니 오류가 발생한다.- unique_ptr은 한 객체에 대한 소유권이 한 포인터에만 있어야하니까 이와 같은 상황도 방지해주는 것!
- 스마트 포인터인 unique_ptr은 scope를 벗어나면 알아서 자동으로
delete
되기 때문에 이렇게 프로그래머가delete
을 명시해주는게 컴파일 오류가 되는건 아니지만 큰 문제가 생길 수 있다. 이미delete
되었는데 또delete
하려는 시도가 될 수 있어서!
unique_ptr을 함수 파라미터로 넘겨주는 경우
void doSomething2(std::unique_ptr<Resource>
&
res) 👉 unique_ptr을 함수 파라미터로 넘겨 받을 때 레퍼런스로 받을 것을 권장한다.
void doSomething2(std::unique_ptr<Resource> & res) // ✨ unique_ptr 를 인수로 받을 땐 레퍼런스로 받기를 권장함
{
res->setAll(10);
}
int main
{
{
auto res1 = std::make_unique<Resource>(5);
res1->setAll(1);
res1->print();
doSomething2(res1); // ✨ unique_ptr인 res1을 매개변수 res가 참조하게 된다.
res1->print();
}
}
unique_ptr reference
가 아닌 그냥unique_ptr
로 받으면 컴파일 오류가 난다.
- unique_ptr 은 가리키는 객체에 대해 소유권이 유일 해야 해서 '복사'를 금지하기 때문이다.
- 두 unique_ptr이 한 객체에 대한 소유권을 동시에 가지 우려가 있기 때문
void doSomething2(std::unique_ptr<Resource> res) // 레퍼런스가 아닌 그냥 unique_ptr으로 받으려고 하고 있다.
{
res->setAll(100);
}
int main
{
{
auto res1 = std::make_unique<Resource>(5);
res1->setAll(1);
res1->print();
doSomething2(res1); // ❌컴파일 오류 발생. res = res1 복사를 금지하기 때문에.
res1->print();
}
}
unique_ptr 을 인수로 받을 때 레퍼런스로 받지 않고 일반 unique_ptr로 받을거라면 인수를
std::move
로 넘겨 소유권을 이전+박탈 하여 받아보자.
void doSomething2(std::unique_ptr<Resource> res) // 레퍼런스가 아닌 그냥 unique_ptr으로 받으려고 하고 있다.
{
res->setAll(100);
res->print();
}
int main
{
{
auto res1 = std::make_unique<Resource>(5);
res1->setAll(1);
res1->print();
std::cout << std::boolalpha;
std::cout << static_cast<bool>(res1) << std::endl; // true 출력
doSomething2(std::move(res1)); // std::move 로 res1의 소유권을 res로 이전한다. 그러나 res1의 소유권은 박탈되어 nullptr이 되버린다는 문제가 생김
std::cout << std::boolalpha;
std::cout << static_cast<bool>(res1) << std::endl; // false 출력. res1가 nullptr이 되어 버림
}
}
res = std::move(res1)
이 되는 것이나 마찬가지.- 객체의 소유권이
res1
에서res
로 옮겨가는 것이니 유일한 소유권이 보장되어 문제 없다. - 그러나 이렇게 되면
res1
의 소유권이 박탈되어 nullptr이 된다는 문제가 생긴다.- 또한
res
가 함수의 매개변수이기 때문에 함수가 끝나면 해당 객체가delete
되어버린다.
- 또한
- 객체의 소유권이
res1
이 원래 소유했던 객체를 잃고 싶지 않다면 그 객체의 소유권을 이전 받은res2
가 함수가 종료되어delete
되기 전에res1
에게 다시 그 객체를 리턴해주면 된다!
auto doSomething2(std::unique_ptr<Resource> res) // 레퍼런스가 아닌 그냥 unique_ptr으로 받으려고 하고 있다.
{
res->setAll(100);
res->print();
return res; // ✨객체를 다시 리턴한다.
}
int main
{
{
auto res1 = std::make_unique<Resource>(5);
res1->setAll(1);
res1->print();
std::cout << std::boolalpha;
std::cout << static_cast<bool>(res1) << std::endl; // true 출력
res1 = doSomething2(std::move(res1)); // res1의 소유권이 박탈되나 소유권이 박탈된 그 객체를 다시 리턴받으므로써 문제 없게 된다!
std::cout << std::boolalpha;
std::cout << static_cast<bool>(res1) << std::endl; // true 출력
}
}
res1
가res
에게 소유권을 넘겨주어 소유권이 박탈되었지만res1
가 함수가 끝날 무렵에 다시 그 객체를res1
에게 리턴해주어 문제가 해결된다!
get()
함수를 사용하여 일반 포인터로 잠시 변환하여 넘길 수도 있다.
void doSomething2(Resource * res) // 일반포인터
{
res->setAll(100);
res->print();
}
int main()
{
auto res1 = std::make_unique<Resource>(5);
doSomething2(res1.get());
}
res1.get()
- unique_ptr인
res1
을 unique_ptr이 아닌 일반 포인터로서 넘기고 있기 때문에res
와res1
은 같은 객체를 가리키게 되더라도 문제가 없다.
- unique_ptr인
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기