스마트 포인터
스마트 포인터를 이용하면 동적할당된 메모리를 알아서 지워준다
3가지 종류 존재
unique_ptr
shared_ptr
weak_ptr
unique_ptr
- int *a = new int(5);를 스마트 포인터를 이용하여 동적할당해보자
#include <iostream>
#include <memory>
using namespace std;
int main() {
// int *a = new int(5);를 스마트 포인터를 이용하여 생성해보자
// 다음은 불가능
// unique_ptr<int> a = new int(5);
unique_ptr<int> a(new int(5));
}
unique_ptr
a = new int(5);은 불가능함 - 이 경우 변환 생성자가 호출되는데, 스마트 포인터의 경우 묵시적 형변환이 불가능하기 때문
따라서 생성자를 이용해야 한다
- unique_ptr
a(new int(5));
- unique_ptr
a가 scope(위 예시에서는 메인함수)를 벗어나면 a가 사라지면서 a가 가리키는 객체가 사라진다
생성자와 소멸자를 통해서 작동 과정을 실제로 확인해보자
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test() { cout << "생성자" << endl; }
~Test() { cout << "소멸자" << endl; }
};
int main() {
cout << "main 함수 시작" << endl;
unique_ptr<Test> a(new Test);
cout << "main 함수 종료" << endl;
}
- delete를 적어주지 않았는데도 스마트 포인터가 객체를 알아서 삭제해준다
왜 unique일까?
여러 개의 포인터가 같은 객체를 가리킬 수 없게 막아뒀기 때문
- a, b, c라는 포인터가 있을 때, a, b, c는 같은 객체를 가리킬 수 없다
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test() { cout << "생성자" << endl; }
~Test() { cout << "소멸자" << endl; }
};
int main() {
cout << "main 함수 시작" << endl;
unique_ptr<Test> a(new Test);
/* 다음은 불가능
// unique_ptr<Test> b(a);
// b = a
*/
cout << "main 함수 종료" << endl;
}
b라는 포인터가 a가 가리키는 객체를 가리키려고 하고 있기 때문에 오류가 발생한다
- unique_ptr
b(a); 그리고 b = a;는 불가능하다
- unique_ptr
메모리 소유권
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test() { cout << "생성자" << endl; }
~Test() { cout << "소멸자" << endl; }
};
int main() {
int *a = new int(5);
int *b = a; // 얕은 복사
cout << *b << endl;
delete a;
// 한 번 더 해제하면 안된다
// delete b;
}
a와 b 중 객체를 삭제할 의무를 가진게 메모리를 해제해줘야 한다
이런 것을 메모리 소유권이라고 한다
위 예시에서는 a가 객체의 메모리 관리를 전담해서 하고 있다.
b는 메모리 소유권을 가지고 있지 않다
int main() {
int *b;
if (...) {
int *a = new int(5);
/// ...
b = a; // 소유권 이전
}
cout << *b << endl;
// delete a를 할 수가 없다.
// 메모리 소유권이 b로 이전되어 b에서 해제해준다
delete b;
}
if문이 끝나면 a 포인터가 사라지기 때문에 5의 값을 갖는 객체를 접근할 수 있는 포인터가 b밖에 없다.
- 따라서 b를 통해서만 delete를 할 수 있다.
- b = a;에서 메모리 소유권을 이전해준다
int main() {
int *a = new int(5);
int *b = a; // 소유권 이전이라고 "해석"
cout << *b << endl;
delete b
}
아까와 동일한 코드. 위에서는 delete a;를 해줬다
여기서는 delete b로 해제하고 있다.
코드는 똑같은데 위에서는 a로 해제하고, 여기서는 b로 해제하고 있다.
동일한 코드인데 메모리 소유권이 다를 수가 있나?
소유권이라는 개념은 의미론적인 개념이다. 문법적으로 소유권이 넘어갔다는 것을 알 수 있는 방법은 없음
int *b = a; 에서 소유권 이전이라고 "해석" 할 뿐이다 (semantic. 의미를 해석)
스마트 포인터를 사용하면 소유권을 문법적인 차원에서 명시적으로 드러나게 할 수 있다
int main() {
unique_ptr<int> a(new int(5));
// b에게 소유권을 이전하고 싶으면 a가 5를 갖는 객체와 연결을 끊어줘야 한다
unique_ptr<int> b(a.release()); // 소유권 이전
// release() : a가 가지고 있는 소유권을 포기하고 다른 포인터로 넘겨주겠다.
cout << *b << endl;
}
unique_ptr이기 때문에 b에게 소유권을 이전하고 싶으면 a가 5를 갖는 객체와 연결을 끊어줘야 한다
unique_ptr
b(a.release()); 에서 소유권 이전 release() : a가 가지고 있는 소유권을 포기하고 다른 포인터로 넘겨주겠다
그리고 소유권 이전을 안하고 참조를 복사할 수 있는 방법은 없다.
- 소유권 이전 안하고 unique_ptr
b(a); 이런 것은 불가능
- 소유권 이전 안하고 unique_ptr
reset()
int main() {
unique_ptr<int> a(new int(5));
a.reset(new int(6));
}
reset()은 재설정을 한다는 의미이다.
- a가 가리키는 객체를 다른 객체로 바꾼다는 것
클래스의 생성자와 소멸자를 통해서 동작을 확인해보자
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test(int x) : x(x) { cout << "생성자" << endl; }
~Test() { cout << "소멸자" << endl; }
int GetX() const { return x; }
private:
int x;
};
int main() {
unique_ptr<Test> a(new Test(5));
cout << a->GetX() << endl;
cout << "====" << endl;
a.reset(new Test(6));
cout << a->GetX() << endl;
cout << "====" << endl;
}
- 실행 결과
생성자
5
====
생성자
소멸자
6
===
소멸자
- new Test(6) 객체가 먼저 생성되고 Test(5) 객체가 삭제된다
shared_ptr
- 여러 포인터가 한 객체를 공유할 수 있다
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test(int x) : x(x) { cout << "생성자" << endl; }
~Test() { cout << "소멸자" << endl; }
int GetX() const { return x; }
private:
int x;
};
int main() {
// 방식 1
shared_ptr<Test> a = make_shared<Test>(5);
// 방식 2
shared_ptr<Test> a(new Test(5));
shared_ptr<Test> b = a;
}
shared_ptr를 만드는 방식에는 두 가지 방법이 있다.
make_shared 이용
- shared_ptr
a = make_shared (5);
- shared_ptr
생성자 이용
- shared_ptr
a(new Test(5));
- shared_ptr
shared_ptr는 복사 생성, 복사 대입 모두 가능하다
shared_ptr
b = a; shared_ptr
b(a); 여러 포인터가 한 객체를 가리킬 수 있게 된다
shared_ptr은 use-count라는 특별한 값을 저장하여, 이 객체를 몇 개의 포인터가 가리키고 있는지 관리한다
a, b가 같은 객체 Test(5)를 가리키는 경우 use-count의 값은 2
a가 사라진 상황에서는 use-count의 값은 1
b가 사라진 상황에서 use-count는 0
b.use_count()을 통해서 실제 use-count 값을 확인해볼 수도 있다
int main() {
shared_ptr<Test> a(new Test(5));
{
shared_ptr<Test> b = a;
cout << a.use_count() << endl;
cout << b.use_count() << endl;
}
cout << a.use_count() << endl;
}
- 실행 결과
생성자
2
2
1
소멸자
weak_ptr
weak, 약하다: 소유권은 없지만 어떤 객체를 가리키는 것은 할 수 있다.
a가 가리키는 Test(5)를 weak 포인터 b가 가리킬 때
- a가 삭제되면 use_count는 0이 된다
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test(int x) : x(x) { cout << "생성자" << endl; }
~Test() { cout << "소멸자" << endl; }
int GetX() const { return x; }
private:
int x;
};
int main() {
weak_ptr<Test> b;
{
shared_ptr<Test> a(new Test(5));
b = a;
cout << a.use_count() << endl;
cout << b.use_count() << endl;
}
cout << b.use_count() << endl;
// 다음은 불가능
// cout << b->GetX() << endl;
}
- 실행 결과
생성자
1
1
소멸자
0
마지막 줄 cout << b.use_count() << endl;에서
b는 이미 소멸된 객체를 가리키고 있다.
cout << b->GetX() << endl; 이런 것은 불가능한다
b는 가리키는 객체가 소멸된 Dangling pointer 이기 때문이다
대신에 b가 가리키는 객체가 존재하는지 확인할 수 있는 방법이 존재한다
int main() {
weak_ptr<Test> b;
{
shared_ptr<Test> a(new Test(5));
b = a;
cout << a.use_count() << endl;
cout << b.use_count() << endl;
}
cout << b.use_count() << endl;
if (!b.expired()) {
cout << b.lock()->GetX() << endl;
}
}
b가 가리키는 객체가 소멸되지 않았다면 b.lock()에서 shared_ptr를 받아와서 GetX()를 호출할 수 있다.
반대로 만약 b가 가리키는 대상이 이미 소멸했다면 b.lock()은 널 포인터를 리턴한다
참조) 두들낙서 C/C++ 강좌
'Programming Language > C++' 카테고리의 다른 글
[Modern C++] std::move, std::forward (0) | 2024.06.30 |
---|---|
[C++] 클래스와 함수의 friend 선언 (0) | 2024.06.29 |
[Modern C++] 함수 객체와 람다식 (0) | 2024.06.28 |
[C/C++] 함수 포인터 (0) | 2024.06.28 |
[C++] 예외 처리(Exception handling) (0) | 2024.06.28 |