본문 바로가기
개발 (언어)/C++

[Modern C++] shared_ptr

by 진현개발일기 2024. 3. 31.

■ 기존 포인터의 문제

동적 할당을 하고 메모리 해제(delete)를 하지 않으면 메모리 릭(Memory leak)이 일어나서 언젠가 메모리를 모두 고갈하여 프로그램이 터지는 일이 발생할 수 있다.


이를 자동으로 관리해주기 위해 template 문법을 사용하여 우리가 임의로 wrapper 클래스를 만들어 이용할 수도 있다.

이럴 경우 Wrapper클래스가 소멸될 때 자동으로 _ptr가 담고있는 주소를 따라들어가 k1 메모리를 해제할 것이다.

하지만 객체를 하나가 아니라 다수가 참조하고 있을 때 이런 Wrapper클래스를 사용하고 있어도 아래와 같은 문제가 발생할 수 있다.

 

[기본 포인터의 문제 예]

화살을 쏴서 화살이 목적지에 도달하고 있는데 목적지가 delete 되었을 때 등등 참조해야하는 메모리가 중간에 해제되었을 때 크래쉬 혹은 메모리 오염이 일어나게 된다. (Use-After-Free 버그)


여러 가지 방법이 있겠지만 대표적으로 객체 포인터를 갖고있지않고 타겟의 id를 가진 채 ObejctManager를 통해 우회해서 체크하거나 그래야 하는데 하지만 생포인터를 고집해서 만들겠다면 실제로 메모리가 해제되었는지를 확인하기가 어렵다. 


이것이 일반 포인터의 한계점이다. 그럼에도 일반 포인터를 사용한다면 생명주기가 꼬여버려서 큰 문제가 발생할 수 있다.

그래서 요즈음 모던 C++로 넘어오면서 생 포인터를 사용하지 않고 거의 다 스마트포인터를 사용하고 있다. 

(언리얼 포함)

■ 스마트 포인터

일반 포인터에 추가적으로 여러 가지 기법을 섞어서 정책에 따라 자동으로 포인터를 관리하도록 하는 것을 '스마트 포인터'라고 한다.

[스마트 포인터의 종류]

(1) shared_ptr
(2) weak_ptr
(3) unique_ptr

이 중에서 shared_ptr의 사용 비중이 99%정도 된다고 한다.

■ shared_ptr (★★)

 

나를 지금 몇명이 추적하고 있는지 기억하는 포인터이다.

 

이와 같이 refCount를 둬서 몇 명이 참조하는지를 확인해야 한다.

 

하지만, 보통은 클래스 내에 일반 변수로 매핑하는 것이 아니라, RefCountBlock이라는 클래스 하나를 만들어서 int _refCount 대신 RefCountBlock* _block;를 갖고 있게 된다.


이때 복사 생성자와 복사 대입 연산자를 아래와 같이 만들어줘야 한다.

 

이럴 경우 복사 생성 혹은 대입이 일어날 때마다 나를 참조하고 있는 개체가 몇 개 인지 확인할 수 있고 
아래와 같이 코드를 작성했을 때마다 자동적으로 block의 count가 증가하게 된다.

 

그리고 소멸자에는

 

와 같이 처리해주면 우리가 매 번 delete를 해줄 필요 없이 C#에서 작업하던 대로 작업을 하다보면 알아서 적절한 시기에 메모리가 해제 될 것이다.

 

■ C++ shared_ptr

위와 같이 우리가 클래스를 만들어서 사용할 수 있지만 C++ 자체에서 shared_pointer를 제공해주기도 한다.
shared_ptr<Knight> k5; 이와 같이 선언 가능.

 

(1) new 할당

 

(2) make_shared 함수 호출

 

 

※ 위 두 가지 중에선 make_shared를 사용하는 것이 일반적으로 더 좋다.

이유는 아래와 같다.

이유 설명
메모리 관리 효율성 'make_shared'함수는 객체와 객체를 관리하는 컨트롤 블록을 하나의 메모리 블록에 함께 할당한다.

이로 인해 추가적인 메모리 할당 및 해제를 줄일 수 있어서 메모리 관리 효율성이 향상된다.
성능 'make_shared'를 사용하면 메모리 할당과 클래스 초기화를 한 번에 처리한다.
이는 메모리 할당을 줄이고 생성 비용까지 최적화함으로써 성능을 개선시켜준다.
예외 안정성 'make_shared' 함수는 예외가 발생할 경우 메모리 누수(Memory Leak)를 방지하기 위해 할당된 메모리를 해제한다. 이는 예외 안정성을 제공하는데 도움이 된다.
가독성 코드가 더 간결해지고 가독성이 증대된다.

 

shared_ptr를 타고 들어가면 -> 오퍼레이터는 내부의 get()함수를 통해 호출된다. 

 


(★) 만약 위와 같이 shared_ptr로 개체를 관리할거면 이제부터 모두 shared_ptr를 이용해야한다. 이걸 일반 ptr랑 섞어쓰면 프로그램이 예상치 못한 문제가 발생할 수 있다.

 

[함수에 인자로 shared_ptr를 넘길 때 참조로 넘겨도 되나?]

1. 그냥 넘길 때 refCount는 우리가 생각하던 대로 +1될 것이고 함수가 끝날 때 -1이 될 것이다.
2. 하지만 클래스의 복사 비용을 생각했을 때 부담스러워서 &를 붙이는 경우와 같이 &를 붙여 참조 형태로 넘겨도 상관이 없다. 다만 이때는 refCount는 +가 안될 것이다.
3. 웬만하면 모든 포인터는 shared_ptr로 선언하고 함수로 넘길 때 refCount를 증가시키고 감소시키는 연산, 그리고 복사 비용 등을 아끼기 위해 참조 형태로 넘기는 것이 일반적이다.
4. 그 외에는 일반 포인터 사용하듯이 사용하면 된다 (-> 오퍼레이터)  그러면 C#으로 작업하는 것과 동일해진다.
5. (★) 나중에 shared_ptr를 직접 만드는 연습을 할 때는 Thread Safe를 위해 _refCount를 기본 int형이 아닌 atomic<int> 형식으로 선언 해줘야 한다. C++의 shared_ptr는 atomic을 이용해 만들어져 있고
단점으론 속도가 느려져서 언리얼에선 표준에서 사용하는 shared_ptr를 사용하지 않고 본인만의 버전으로 만들어서 사용하곤 한다.

 

 

[요약]

shared_ptr은 일반 포인터와 동작은 비슷하나, 내부적으로 참조 카운트라는 걸 이용해서 객체를 몇명이 추적하고 있는지를 기억하고 있다. 그래서 shared_ptr이 복사되는 순간에는 Reference Count가 증가하게 되고 소멸이 될 때는 Reference Count가 감소하게 된다.

 

만약 해당 메모리를 추적하고있는 모든 Shared_ptr이 소멸이되어 Ref Count가 0이 되었을 때 실제로 참조하고있는 메모리가 해제가 된다.

728x90

'개발 (언어) > C++' 카테고리의 다른 글

[MultiThread] 쓰레드 생성  (1) 2024.04.07
[Modern C++] weak_ptr, unique_ptr  (0) 2024.03.31
[Modern C++] rvalue-ref (오른값 참조)  (0) 2024.03.31
[Modern C++] Unicode, MBCS, WBCS  (1) 2024.03.29
[Modern C++] String  (0) 2024.03.29