이이프 2022. 9. 25. 11:56
728x90

1. unique_ptr 생성 방법

다음은 Simple 객체를 힙에 할당한 뒤 이를 해제하지 않아 메모리 누수가 현상이 발생하는 경우이다.

void leaky()
{
    Simple* mySimplePtr = new Simple();
    mySimplePtr->go();
}

 

코드를 작성할 때 동적으로 할당한 메모리를 제대로 해제한다고 여기기 쉽다.

하지만 그렇지 않을 가능성이 훨씬 높다. 다음 함수를 보자.

void couldBeLeaky()
{
    Simple* mySimplePtr = new Simple();
    mySimplePtr->go();
    delete mySimplePtr;
}

이 함수는 Simple 객체를 동적으로 할당해서 사용하고 나서 delete를 호출한다.

이렇게 해도 메모리 누수가 발생할 가능성은 남아 있다.

go() 메서드에 익셉션(에러)이 발생하면 delete가 실행되지 않기 때문이다.

 

unique_ptr 구현

앞에서 본 두 코드 모두 unique_ptr로 구현해야 한다.

그러면 객체에 대해 delete를 직접 호출하지 않아도 된다.

unique_ptr 인스턴스가 스코프를 벗어나면 소멸자가 호출될 때 Simple 객체가 자동으로 해제된다.

void notLeaky()
{
    auto mySimpleSmartPtr = make_unique<Simple>();
    mySimpleSmartPtr->go();
}

이 코드는 C++14부터 제공하는 make_unique()와 auto 키워드를 동시에 적용했다.

그래서 Simple이라는 포인터 타입만 지정했다.

Simple 생성자에서 매개변수를 받는다면 make_unique() 호출문의 소괄호 사이에 지정하면 된다.

예를 들어 Simple(int, int)라면 make_unique<Simple>(1, 2);와 같이 생성자 인수를 전달할 수 있다.

 

make_unique()를 지원하지 않는 컴파일러를 사용한다면 다음과 같이 unique_ptr로 생성한다.

unique_ptr<Simple> mySimpleSmartPtr(new Simple());

C++ 이전에는 타입을 단 한 번만 지정하기 위해서 뿐만 아니라 안전을 위해 반드시 make_unique()를 사용해야 했다.

 

 


 

2. unique_ptr 사용 방법

스마트 포인터는 일반 포인터와 똑같이 *나 ->로 역참조 한다.

예를 들어 앞에서 본 예제에서 go() 메서드를 호출할 때 -> 연산자를 사용했다.

mySimpleSmartPtr->go();

 

다음과 같이 일반 포인터처럼 작성해도 된다.

(*mySimpleSmartPtr).go();

 

get() 메서드를 이용하면 내부 포인터에 직접 접근할 수 있다.

일반 포인터만 전달할 수 있는 함수에 스마트 포인터를 전달할 때 유용하다. 예를 들어 다음과 같은 함수가 있다고 하자.

void processData(Simple* simple) { /* 스마트 포인터를 사용하는 코드 */ }

그러면 이 함수를 다음과 같이 호출할 수 있다.

    auto mySimpleSmartPtr = make_unique<Simple>();
    processData(mySimpleSmartPtr.get());

 

reset()을 사용하면 unique_ptr의 내부 포인터를 해제하고, 필요하다면 이를 다른 포인터로 변경할 수 있다.

예를 들면 다음과 같다.

    mySimpleSmartPtr.reset();             // 리소스 해제 후 nullptr로 초기화
    mySimpleSmartPtr.reset(new Simple()); // 리소스 해제 후 새로운 Simple 인스턴스로 설정

 

release() 를 이용하면 unique_ptr와 내부 포인터의 관계를 끊을 수 있다.

release() 메서드는 리소시에 대한 내부 포인터를 리턴한 뒤 스마트 포인터를 nullptr로 설정한다.

그러면 스마트 포인터는 그 리소스에 대한 소유권을 잃으며, 리소스를 다 쓴 뒤 반드시 직접 해제해야 한다.

예를 들면 다음과 같다.

    Simple* simple = mySimpleSmartPtr.release(); // 소유권을 해제한다.
    // simple 포인터를 사용하는 코드
    delete simple;
    simple = nullptr;

unique_ptr은 단독 소유권을 표현하기 때문에 복사할 수 없다.

std::move() 유틸리티를 사용하면 하나의 unique_ptr를 다른 곳으로 이동할 수 있는데,

복사라기보다는 이동의 개념이다.

 

 


 

3. unique_ptr와 C 스타일 배열

unique_ptr 은 기존 C 스타일의 동적 할당 배열을 저장하는 데 적합하다.

예를 들어 정수 10개를 가진 C 스타일의 동적 할당 배열을 다음과 같이 표현할 수 있다.

    auto myVariableSizedArray = make_unique<int[]>(10);

이렇게 unique_ptr로 C 스타일의 동적 할당 배열을 저장할 수 있지만,

이보다는 std::array나 std::vector와 같은 표준 라이브러리 컨테이너를 사용하는 것이 좋다.

 

 


 

4. 커스텀 제거자

기본적으로 unique_ptr은 new와 delete로 메모리를 할당하거나 해제한다.

하지만 다음과 같이 방식을 변경할 수 있다.

int* malloc_int(int value)
{
    int* p = (int*)malloc(sizeof(int));
    *p = value;
    return p;
}

int main()
{
    unique_ptr<int, decltype(free)*>myIntSmartPtr(malloc_int(42), free);
    return 0;
}

이 코드는 malloc_int()로 정수에 대한 메모리를 할당한다.

unique_ptr은 메모리를 표준 free() 함수로 해제한다.