C++ 스마트 포인터

2021. 12. 28. 00:21C++

포인터를 사용했으면 바로 delete를 사용해라

 

스마트 포인터 -> delete를 직접 호출할 필요가 없다. 가비지 컬렉터보다 빠르다

1. unique_ptr (많이 사용)

포인터(원시)를 단독으로 소유

원시 포인터는 누구하고도 공유하지 않음.

-> 복사나 대입 불가

unique_ptr가 범위를 벗어날 때 원시 포인터는 지워짐(delete)

unique포인터를 사용하기 적합한 경우

1) 클래스에서 생성자/소멸자 -> 앞으로 소멸자 사용X

2) 지역 변수 선언할 때

3) STL 벡터에 포인터 저장할 때

C+11에서는 포인터를 하나 만든 후 unique포인터 2개에 각각 대입을 하고 하나를 delete하면 모든 정보가 날라가고 남은 1개의 unique포인터를  delete하면 이미 지워진 데이터를 지우려고한다. -> 문제 발생

=> C++14이후 make_unique사용

-> std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);

std::make_unique의 기능 : 주어진 매개변수, 자료형으로 new 키워드를 호출

- 둘 이상의 std::unique_ptr가 원시 포인터를 공유할 수 없도록 막음 (사실상 핵심)

유니크 포인터

C++11

기본 : std::unique_ptr<Vector> vector(new Vector(10.f, 30.f));

배열 : std::unique_ptr<Vector[]> vectors(new Vector[20]);

C++14

기본 : std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);

배열 : std::unique_ptr<Vector[]> vectors = std::make_unique<Vector[]>(20);

 

reset()함수

vector.reset(new Vector(20.f,40.f)); -> 재 할당(기존 메모리 delete후 새로 할당)

vector.reset(); -> nullptr 즉, vector = nullptr;과 동일함 (가독성을 위해 후자를 선택)

=> 포인터를 교체한다. std::unique_ptr가 재설정 될 때 소유하고 있는 원시 포인터는 자동 소멸

 

get()함수

원시 포인터를 반환해준다.

ex) Vector* ptr = vector.get(); //원시포인터를 받아 delete하면 난리 난다.

 

release()함수

놓아준다. -> 기존 원시 포인터를 소멸시키지 않고 메모리에 놔두며, 포인터는 nullptr을 가리킨다.

ex) Vector* vectorPtr = vector.release(); -> vectorPtr은 원시포인터를 가리키며, vector는 nullptr을 가리킴

=> 매우 안좋은 방식

 

unique_ptr은 포인터 복사가 되지 않지만 소유권은 옮길 수 있다.

ex)

std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f,30.f);

std::unique_ptr<Vector> anotherVector(std::move(vector));

vector는 nullptr을 가리키게 되고 anotherVector는 기존 vector가 가리키던 10.f,30.f 원시포인터를 가리킴

(const를 사용하면 못 옮김)

std::move()함수

- 개체 A의 모든 멤버를 포기하고 그 소유권을 B에게 주는 방법이다.

- 메모리 할당, 해제가 일어나지 않는다.

- 간단하게, A의 모든 포인터를 B에게 대입하고 A는 nullptr을 넣는다고 생각.

 

이제 모두 유니크 포인터를 사용한다.

직접 메모리 관리하는 것 보다 빠르다.

RAII(Resource Acquisition Is Initialization)원칙에 잘 들어맞는다.

1) 자원 할당은 개체 수명과 관련 있다.

2) 생성장에서 new, 소멸자에서 delete

3) std::unique_ptr 멤버 변수가 이를 해준다.

실수하기 어렵기 때문에 모든 곳에서 사용하자

 

2. shared_ptr

자동 메모리 관리 주로 사용되는 두 가지 기법

1) 가비지 컬렉션 (Garbage Collection, GC) -> 메모리 누수가 없진 않다. (Java, C#에서 지원)

-> 보통 트레이싱 가비지 컬렉션을 의미

- 메모리 누수를 막으려는 시도였다.

- 주기적으로 컬렉션 실행

- 충분한 여유 메모리 없을 때 컬렉션 실행(수동으로도 가능)

- 매 주기마다 GC는 루트("root")를 확인 : 전역 변수, 스택, 레지스터

- 힙에 있는 개체에 루트를 통해 접근이 가능한지 판단

- 접근 불가는 가비지로 간주하여 해제

가비지 컬렉션의 문제점 :

사용되지 않는 메모리 즉시 정리X, 메모리 해체해야 하는지 판단하는 동안 앱이 멈추거나 버벅거릴 수 있다.

-> 부드럽게 화면이 넘어가야하는 게임같은 프로그램에서는 자주 사용하지 않는다.

-> 게임같은 프로그램에서는 C언어 처럼 프로그램을 짜는 방식으로 코드를 사용하게 된다.

2) 참조 카운팅(Reference Counting, RefCounting) (Swift, 애플 Objective-C에서 지원)

수동 참조 카운팅은 COM에서 지원해준다. (ex) Direct X)

std::shared_ptr는 이걸 자동으로 해준다.

참조 카운팅의 문제점 :

참조 횟수가 너무 자주 바뀜 -> 멀티 쓰레드 환경에서 안전하려면 lock이나 원자적 연산이 필요

순환 참조 : 개체 A가 B를 참조 개체 B가 A를 참조 -> 절대 해제되지 않는다.(메모리 누수 발생 가능)

 

std::shared_ptr

- 두 개의 포인터 소유

1) 데이터를 가리키는 포인터 (데이터)

2) 제어 블록을 가리키는 포인터 (강한 참조, 약한 참조 횟수, Allocator, delete, 등등)

- 참조 카운팅 기반이다.

- std::unique_ptr과 달리 포인터를 다른 std::shared_ptr과 공유할 수 있다.

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

//shared 포인터 만들기

template<class T, class... Args>

shared_ptr<T> make_shared(Args&&... args);

//포인터 소유권 공유하기

1) shared_ptr& operator= (const shared_ptr& x) noexcept;

2) 

template <class U>

shared_ptr& operator= (const shared_ptr<U>& x) noexcept;

//포인터 재설정하기

void reset() noexcept;

-> 원시 포인터 해제(참조 카운터 1감소) nullptr을 대입하는 것과 같다.

ex) vector.reset(); == vector=nullptr;

 

가비지 컬레션과 참조 카운팅을 사용하면 전통적 메모리 누수는 없다.

-> delete를 잊었다. 라는 누수는 없다.

-> 하지만 메모리 누수가 없는게 아니다. 순환 참조같은 경우 메모리 누수가 발생한다. (발견해도 고치기 어렵다)

 

가비지 컬렉션

- 사용하기 쉽다.

- 실시간 또는 고성능 프로그램에 부적합

참조 카운팅

- 사용하기 쉽다

- 실시간 또는 고성능 프로그램에 적합

- 멀티 쓰레드 환경에서 순수한 포인터보다 훨씬 느리다.

 

3. weak_ptr

 

강한 참조 : 개체 A가 개체 B를 참조할 때, 개체 B는 절대 소멸되지 않음. (일반적인 참조는 강한 참조)

강한 참조 수를 저장하기 위해 강한 참조 카운트 사용

즉, 누군가가 나를 참조하지 않으면 소멸한다. 나를 참조하면 소멸하지 않는다.

 

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

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

-> 순환 참조 문제의 해결책

 

공유 포인터에서부터 약한 포인터를 만든다.

template <class U>

weak_ptr& operator= (const shared_ptr<U>& x) noexcept;

 

//약한 포인터로 공유 포인터 만들기

std::shared_ptr<T> lock() const noexcept;

 

ex)

std::shared_ptr<Person> owner = std::make_shared<Person>("Lulu");

std::weak_ptr<Person> weakOwner = owner;

std::shared_ptr<Person> lockedOwner = weakOwner.lock();

 

weakOwner.expired()를 이용하여 공유 포인터가 존재하는지 확인 가능

존재하면 false 없으면 true

-> 허나 안전한가??

-> 멀티쓰레드 환경에서 확인 후 사용하려는 중간과정에 메모리를 소멸시킬 수 있기 때문

=> 사용하기 전에 lock을 거는 것이 맞다.

 

강한 참조는 직접적으로 사용할 수 있다.

약한 참조는 직접적으로 사용할 수 없지만 lock을 이용하여 shared_ptr이 여전히 존재하는지 확인 가능

 

 

'C++' 카테고리의 다른 글

C++ constexpr  (0) 2022.03.02
이동 생성자 및 이동 대입 연산자  (0) 2022.02.19
C++ STL  (0) 2021.12.27
C++ 새로운 자료형  (0) 2021.12.26
C++ 11/14/17/...의 키워드  (0) 2021.12.24