예외처리 과정에서 delete 해야 했던 것을 못하고 넘어가게 되는 경우가 발생
→ 메모리 누수(memory leak)
하지만 C++에서는,
예외가 발생해 함수에서 빠져나가더라도,
그 함수의 스택에 정의된 모든 객체는 자동으로 소멸자 호출됨
→ 이 과정을 stack unwinding(스택 풀기)이라고 한다
이 아이디어를 차용하여,
포인터를 객체로 감싸고, 그 객체의 소멸자에서 delete를 자동으로 호출하도록 설계했다.
자원(포인터, 파일, 락 등)을 생성자에서 획득
해당 객체가 scope을 벗어나면 소멸자에서 자원 자동 해제
즉, 자원을 객체의 생명주기(lifetime)에 묶어버린 것.
특정객체에 유일한 소유권을 부여하는 것이다.
여러 객체가 참조하지 못한다.
소유권이란?
이 객체가 더 이상 필요 없을 때, "누가 메모리에서 해제(delete)할 책임이 있는가?" 에 대한 권한을 의미한다.
unique ptr에선 그 소유권을 특정 객체에게 명확히 한다.
소유권이 중요한 이유
1. Dangling Pointer (허공을 가리키는 포인터): A가 객체를 지웠는데, B가 그 객체에 접근하려 할 때
Double Free (중복 해제): A도 지우고, B도 똑같은 객체를 지우려 할 때 발생
Memory Leak (메모리 누수)
따라서
std::unique_ptr<Class> ptr(new int(35));
일 경우
std::unique_ptr<Class> ptr2 = ptr; //불가능
이 불가능한데 기본적으로 복사 연산자가 없기 때문이다.
그렇기에 소유권을 넘겨주기 위해서는
std::unique_ptr<Class> ptr2 = std::move(ptr);
의 rvalue 형태로 이동시켜줘야 한다.
함수 인자로 전달할 때는 포인터의 형태로 전달해야한다. unique_ptr형태로 전달할 경우 더 이상 유일한 소유권이 아니게 되기 때문이다.
void func(int *a);
int main()
{
std::unique_ptr<int> ptr(new int(35));
func(ptr.get()) //실제 주소값 전달
}
템플릿인자로 전달된 클래스의 생성자 인자들에 직접 완벽한 전달을 한다.
auto ptr = std::make_unique<Class>(1);
vector에 사용할 경우 그래도 유니크 포인터로 전달할 경우 컴파일 에러가 발생한다. -> 벡터에 push_back할 때 복사하여 전달하는데 복사 연산자가 없기 때문이다.
역시 rvalue 형태로 만들어 넣어줘야 한다.
vec.push_back(std::move(ptr));
//or
vec.emplace_back(new Class(1));
유니크 포인터와는 다르게 여러 공유 포인터가 하나의 객체를 가리킬 수 있다.
그럼 delete는 어떻게 할까?
->그 객체를 참고 하고 있는 개수(참조 카운트)가 0이 될 경우 소멸
개수를 저장할때 각각의 포인터 객체에 넣으면 업데이트를 어떻게 시키지?
-> 처음에 제어 블록(control block)을 동적으로 할당하고 각 포인터 객체들이 이를 공유하며 필요한 정보를 얻거나 업데이트 시킴.
std::shared_ptr<int> ptr(new int(10));//
std::shared_ptr<int> ptr2 = ptr;
복사 연산자가 있기 때문에 = 사용 가능하며 위 경우엔 한 객체를 2개가 가리키게 된다.
[ 제어 블록 (ref count 포함) ] ← 여러 shared_ptr이 이걸 공유
↑
[shared_ptr 객체]
↓
[실제 데이터 (int)]
제어 블록(control block)에는 다음이 포함됨:
참조 카운트 (use_count)
삭제자 (deleter)
weak reference count (shared_ptr + weak_ptr 관리용)
일반적으로 공유 포인터를 만들경우 객체 그리고 제어블록에 대한 동적할당 2번이 일어난다. make_shared을 쓸 경우 두 크기를 합친만큼의 동적할당 1번이 일어나므로 속도가 더 빠르다.
std::shared_ptr<A> p1 = std::make_shared<A>();
### ```
#### 주의할 점
인자로 주소값을 전달할 경우 첫 번째 객체로 갖는다고 인식하여 여러 개가 같은 주소값을 가리켜도 각각 1개씩 가리킨다고 생각한다.
-> 즉, 서로 다른 제어블록을 계속 생성함.
```cpp
A* a = new A();
std::shared_ptr<A> ptr1(a);
std::shared_ptr<A> ptr2(a);
-> 참조카운트에 대한 오해로 이미 소멸시킨 객체를 또 소멸시켜 오류가 발생한다.
shared_ptr는 T*만 가지고 있다고 해서 내부적으로 그걸 관리할 수 없음
객체 안에서 자신을 shared_ptr로 다시 만들려면 자신이 어느 제어 블록에 소속돼 있는지 알아야 함
class ex
{
std::shared_ptr<ex> get_shared_ptr() { return std::shared_ptr<ex>(this); }
}
int main() {
std::shared_ptr<ex> pa1 = std::make_shared<ex>();
std::shared_ptr<ex> pa2 = pa1->get_shared_ptr(); //오류 발생
}
->이럴 경우 오류가 발생하는데 이때
enable_shared_from_this 클래스를 상속받아서 사용하면 된다.
class ex: public std::enable_shared_from_this<ex>
{
std::shared_ptr<ex> get_shared_ptr() { return shared_from_this(); }
}
int main() {
std::shared_ptr<ex> pa1 = std::make_shared<ex>();
std::shared_ptr<ex> pa2 = pa1->get_shared_ptr();
}
enable_shared_from_this를 상속하면,
std::weak_ptr를 내부에 저장해두고,
shared_from_this() 호출 시 그걸 shared_ptr로 승격해서 반환함.
weak_ptr는 shared_ptr와 함께 사용하는 보조 스마트 포인터로,
객체를 소유하지 않고 참조만 하기 위한 목적으로 사용된다.
순환 참조 문제를 회피할 수 있는 유일한 안전한 방법
주로 부모-자식 관계, 콜백, 이벤트 리스너, 캐시 등에서 사용
shared_ptr는 참조 카운트를 기반으로 객체를 소멸시킴.
그런데 아래와 같은 경우가 생기면 문제가 생김:
A → (shared_ptr) → B
B → (shared_ptr) → A
서로가 서로를 shared_ptr로 참조하면 참조 카운트가 0이 될 수 없어,
객체가 소멸되지 않는 메모리 누수가 발생한다.
➡ 이걸 해결하려고 나온 게 바로 weak_ptr이다.
#include <iostream>
#include <memory>
struct B;
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::weak_ptr<A> a_ptr; //
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // shared_ptr이었다면 순환 참조 발생
// 둘 다 scope 벗어나면 자동으로 소멸
}
weak_ptr은 객체가 이미 소멸되었을 수도 있기 때문에,
바로 접근하지 못하고 lock() 함수를 통해 접근해야 한다.
std::weak_ptr<A> weak;
if (auto shared = weak.lock()) {
// shared는 유효한 shared_ptr → 객체가 살아 있음
shared->doSomething();
} else {
// 객체는 이미 소멸됨
}
shared_ptr, weak_ptr 모두 내부적으로 제어 블록을 공유함
제어 블록에는 다음 정보가 있음:
use count == 현재 shared_ptr 참조 수
weak count == 현재 weak_ptr 참조 수
deleter 삭제자 정보
객체는 use count == 0일 때 소멸됨
제어 블록 자체는 shared_ptr + weak_ptr 모두 0일 때 메모리에서 해제됨