스마트 포인터(unique, shared, weak)

MwG·2025년 3월 21일

C++

목록 보기
10/21

왜 스마트포인터를 사용하게 됐을까?

예외처리 과정에서 delete 해야 했던 것을 못하고 넘어가게 되는 경우가 발생
→ 메모리 누수(memory leak)

하지만 C++에서는,
예외가 발생해 함수에서 빠져나가더라도,
그 함수의 스택에 정의된 모든 객체는 자동으로 소멸자 호출됨
→ 이 과정을 stack unwinding(스택 풀기)이라고 한다

이 아이디어를 차용하여,
포인터를 객체로 감싸고, 그 객체의 소멸자에서 delete를 자동으로 호출하도록 설계했다.

RAII (Resource Acquisition Is Initialization)

자원(포인터, 파일, 락 등)을 생성자에서 획득
해당 객체가 scope을 벗어나면 소멸자에서 자원 자동 해제

즉, 자원을 객체의 생명주기(lifetime)에 묶어버린 것.

✅Unique Ptr

특정객체에 유일한 소유권을 부여하는 것이다.
여러 객체가 참조하지 못한다.

소유권이란?
이 객체가 더 이상 필요 없을 때, "누가 메모리에서 해제(delete)할 책임이 있는가?" 에 대한 권한을 의미한다.
unique ptr에선 그 소유권을 특정 객체에게 명확히 한다.

소유권이 중요한 이유
1. Dangling Pointer (허공을 가리키는 포인터): A가 객체를 지웠는데, B가 그 객체에 접근하려 할 때

  1. Double Free (중복 해제): A도 지우고, B도 똑같은 객체를 지우려 할 때 발생

  2. 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()) //실제 주소값 전달
}

make_unique

템플릿인자로 전달된 클래스의 생성자 인자들에 직접 완벽한 전달을 한다.

 auto ptr = std::make_unique<Class>(1);
 

컨테이너에 사용

vector에 사용할 경우 그래도 유니크 포인터로 전달할 경우 컴파일 에러가 발생한다. -> 벡터에 push_back할 때 복사하여 전달하는데 복사 연산자가 없기 때문이다.
역시 rvalue 형태로 만들어 넣어줘야 한다.

vec.push_back(std::move(ptr));
//or
vec.emplace_back(new Class(1));

✅shared_ptr

유니크 포인터와는 다르게 여러 공유 포인터가 하나의 객체를 가리킬 수 있다.

그럼 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 관리용)

make_shared

일반적으로 공유 포인터를 만들경우 객체 그리고 제어블록에 대한 동적할당 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);

-> 참조카운트에 대한 오해로 이미 소멸시킨 객체를 또 소멸시켜 오류가 발생한다.

enable_shared_from_this

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

std::weak_ptr

weak_ptr는 shared_ptr와 함께 사용하는 보조 스마트 포인터로,

객체를 소유하지 않고 참조만 하기 위한 목적으로 사용된다.
순환 참조 문제를 회피할 수 있는 유일한 안전한 방법
주로 부모-자식 관계, 콜백, 이벤트 리스너, 캐시 등에서 사용

🔄 순환 참조 문제 (Circular Reference)

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 벗어나면 자동으로 소멸
}

lock() 함수

weak_ptr은 객체가 이미 소멸되었을 수도 있기 때문에,
바로 접근하지 못하고 lock() 함수를 통해 접근해야 한다.

std::weak_ptr<A> weak;
if (auto shared = weak.lock()) {
    // shared는 유효한 shared_ptr → 객체가 살아 있음
    shared->doSomething();
} else {
    // 객체는 이미 소멸됨
}

제어 블록 (Control Block)

shared_ptr, weak_ptr 모두 내부적으로 제어 블록을 공유함
제어 블록에는 다음 정보가 있음:

use count == 현재 shared_ptr 참조 수
weak count == 현재 weak_ptr 참조 수
deleter 삭제자 정보
객체는 use count == 0일 때 소멸됨
제어 블록 자체는 shared_ptr + weak_ptr 모두 0일 때 메모리에서 해제

0개의 댓글