본문 바로가기

CS/Python & C++

스마트 포인터

1. 스마트 포인터 종류
unique_ptr
shared_ptr
weak_ptr

 

2. 스마트 포인터를 왜?
기존 포인터의 문제점
아래와 같이 포인터를 할당했으면 더 이상 포인터가 필요 없을 때,
delete를 해줘야 하는데 프로그래머들이 delete를 자꾸 까먹음…

 

 

 

 

 

 

그런데 스마트 포인터를 쓰면 delete를 호출해 줄 필요가 없다! 또한 가비지 컬렉션 보다 빠르다.

(C++에는 가비지 컬렉이 없긴 하다)

 

3. unique_ptr

3.1 원리 

Object가 생성 될 때 리소스를 할당하고 소유권을 가지고 있으면서 Object가 소멸할 때 리소스를 해제한다.
아래의 코드 처럼 unique_ptr는 따로 delete를 할 필요가 없고 unique_ptr의 소멸자에서 delete가 호출된다. 따라서 scope를 넘어가서 소멸 되면 자동으로 메모리 해제가 이루어진다.

unique_ptr는 누구와도 포인터를 공유하지 않기 때문에 아래의 코드처럼 복사와 대입이 불가능하다.

 

3.2 unqiue_ptr을 언제 쓰는가?

ㄱ. 클래스의 생성자 및 소멸자
소멸자에서 메모리 해제를 해줄 필요가 없어진다.

 

ㄴ. 지역변수
delete 생략 가능!

 

ㄷ. std::vector에 포인터를 저장할 때
아래의 코드처럼 vector 안의 포인터들을 delete 해줄 필요가 없다.

 

3.4 C++14 이후의 uniuqe_ptr

이전 방식의 문제점:

위의 코드 처럼 mv1가 mv2가 같은 원시 포인터 mvCtrl을 가지고 있을 수 있게 됨. 이때 아래 코드와 같이 mv2에 nullptr을 대입하면 값이 날라가게 됨 되는데도 아직 mv1은 mvCtrl을 가지고 있어 mv1이 사라질 때 이미 값이 날아간 것을 다시 없애려고 시도 하면서 버그가 발생함.

 

C++14이후의 방식:
주어진 자료형과 매개변수로 new 키워드를 호출해주는 make_unique의 도입.

make_unique는 아래의 3가지 방법에 대한 것을 모두 컴파일을 불가능하게 하여 위의 기존 C++11에서 생길 수 있는 포인터 공유의 문제를 막아준다.

 

3.4 unique_ptr 배열 만드는 방법 

 

3.5 unique_ptr의 reset, get, release

ㄱ. reset
지니고 있는 원시 포인터를 전환. 매개변수가 없으면 nullptr로 전환.

 

ㄴ. get

지니고 있는 원시 포인터를 반환함.

 

ㄷ. release
지니고 있는 원시 포인터의 소유권을 놓아줌.

 

 

## 20231218

 

3.6 std::move

ㄱ. 메모리 할당과 해제가 일어나지 않음.

ㄴ. B = std::move(A) >> A에 있는 모든 포인터를 B에게 대입하고 A에는 nullptr를 넣는다고 생각하면 된다.

ㄷ. r-value & 이동 생성자와 관련이 있음.

 

3.7 unique_ptr<Test>(new Test()) VS make_unique<Test>()   && uPtr.release() VS move(uPtr)

ㄱ. unique_ptr<Test>(new Test()) VS make_unique<Test>()

Why?

 

ㄴ.uPtr.release() VS move(uPtr)

move 적용 1단계

 

move 적용 2단계

unique_ptr(new Test()) VS make_unique() 은 같다 복습!!

 

 

4. shared_ptr

4.1 자동 메모리 관리 

ㄱ. 가비지 컬렉션 : Java, C# 등에서 사용

ㄴ. 참조 카운팅 : Swift, 애플 Objective C에서 사용 그리고 Direct X, Havok 물리엔진 에서도 사용

 

4.2 가비지 컬렉션

트레이싱 가비지 컬렉션

 

가비지 컬렉션의 문제점:

ㄱ. 사용되지 않는 메모리를 즉시 정리하지 않음

ㄴ. GC가 메모리를 해제해야 하는지 판단하는 동안 어플리케이션이 멈추거나 버벅일 수 있음.

>> 초당 프레임이 중요한 분야에서 사용 불가.
>> 그래서 안드로이드 게임은 가비지를 안만듬

 

4.3 참조 카운팅

가비지 컬렉션과 마찬가지로 개체에 대한 참조가 없을 때, 개체가 해제됨.

어떤 개체 A를 다른 개체 B가 참조할 때 횟수가 개체 A의 참조 횟수가 늘어남

B가 참조를 그만둘 때(ex. B가 범위를 벗어나는 경우) 참조 횟수가 줄어듦

DiriectX에서 수동 참조 카운팅을 지원

std::shared_ptr는 수동 참조 카운팅을 자동으로 해주는 것

멀티 스레딩 환경에서는 자동 참조 카운팅이 수동 참조 카운팅 보다 느려서 속도가 중요할 때는 수동 참조 카운팅을 사용.


4.3.2 참조 카운팅의 문제점 
ㄱ. 참조 횟수는 너무 자주 바뀜
멀티 쓰레드 환경에서 안전하려면, lock이나 원자적(atomic) 연산이 필요
++RefCount보다 확연히 느리다.

ㄴ. 순환(Circular) 참조
개체 A가 B를 참조 && 개체 B가 A를 참조 -> 해제되지 않는다!!

4.3.3가비지 컬렉션 vs 참조 카운팅
ㄱ. 사용하기 확실히 더 쉬움
ㄴ. 실시간 또는 고성능 프로그램에 적합하지 않음
ㄷ. ?보다는 어렵지만 여전히 쉬움
ㄹ. 실시간 또는 고성능 프로그램에 적합
ㅁ. 멀티 스레드 환경에서는 순수한 포인터보다 훨씬 느림

4.4 std::shared_ptr
두 개의 포인터를 소유 
-데이터(원시 포인터)를 가리키는 포인터
-제어 블록(참조 카운트를 가지고 있는 메모리 공간)을 가리키는 포인터

std::unique_ptr과 달리, 원시 포인터를 다른 std::shared_ptr와 공유 할 수 있음.
참조 카운팅 기반

원시 포인터는 어떠한 std::shared_ptr에게도 참조되지 않을 때 소멸

포인터 재설정 하기 (reset)

-원시 포인터를 해제한다. 

-참조 카운트가 1 줄어듦

- nullptr를 대입하는 것과 같

 

shared_ptr의 순환 참조 예시

 

5. weak_ptr

5.1 강한(Strong) 참조와 약한(Weak) 참조
강한 참조 = 개체 A가 개체 B를 참조할 때, 개체 B는 절대 소멸되지 않음을 의미
강한 참조의 수를 저장하기 위해 강한 참조 카운트를 사용
일반적으로 새 인스턴스, 즉 개체에 대한 참조를 만들 때 강한 참조 횟수가 증가함
강한 참조 횟수가 0이 될 때 해당 개체는 소멸됨
shared_ptr는 강한 참조 횟수를 증가시킨다!

 

약한 참조 = 원시 포인터 해제에 영향을 끼치지 않음

약한 참조 카운트는 약한 참조의 수를 저장하는 데 사용됨

약한 참조로 참조되는 개체는 강한 참조 카운트가 0이 될 때 소멸됨

순환 참조 문제의 해결

5.2 weak_ptr의 사용

 

weak_ptr로 shared_ptr 만들어서 사용

weak_ptr는 lock을 써서 std:;shared_ptr가 여전히 존재하는지 확인해야함.

weak_ptr로 순환참조 문제 해결

weak_ptr의 활용

 

 

'CS > Python & C++' 카테고리의 다른 글

C++의 문자열 std::string  (0) 2024.02.15
이동 생성자 & 이동 대입 연산자  (0) 2024.01.29
Circular Dependency (순환 종속성)  (0) 2023.06.28
Mutable과 Immutable  (0) 2023.01.15
객체지향과 클래스  (0) 2022.08.01