C++ Smart Pointer

Seongcheol Jeon·2024년 12월 7일
0

CPP

목록 보기
31/47
post-thumbnail

Smart Pointer의 등장

C++의 대표적인 특징은 메모리를 직접 관리할 수 있다는 점이다. 즉, 필요할 때 메모리를 할당하고 필요하지 않을 때 메모리를 해제할 수 있다. 덕분에 C++ 언어로 만든 프로그램은 메모리를 효율적으로 사용해 성능이 좋지만, 메모리 할당과 해제 등을 개발자가 직접 관리해야 하므로 잘못하면 메모리 누수 같은 문제를 야기할 수도 이싿. 이처럼 C++ 언어의 메모리 관리 기능은 양날의 검이라고 할 수 있다.

최근 프로그래밍 언어들은 메모리 관리가 아주 쉽거나 아예 필요 없는 언어도 많다. C++ 언어도 메모리 관리를 지원하고자 auto_ptr을 제공했지만, 여러 가지 문제로 C++17 부터는 제외되었다.
모던 C++에서는 auto_ptr을 대신해 unique_ptr, shared_ptr, weak_ptr 등 다양한 스마트 포인터(smart pointer)를 제공한다.

RAII 디자인 패턴

스마트 포인터를 이해하려면 RAII라는 디자인 패턴을 알아야 한다. RAIIResource Acquisition Is Initialization의 앞 글자를 따서 만든 단어로, 리소스 할당은 초기화다 라고 직역할 수 있다. 이를 cppreference.com에서 이해하기 쉽게 해설한 내용을 인용하면 다음과 같다.

RAII 패턴은 객체에 접근할 수 있는 모든 곳에서 리소스(메모리, 파일, 식별자 등)를 사용하고자 할 때 항상 사용할 수 있음을 보장한다(리소스의 가용성은 클래스 불변성이기 때무넹 리소스를 사용할 수 있는지 매번 확인해 볼 필요가 없음).
또한 제어 객체의 수명이 끝아면 획득 순서의 역순으로 모든 리소스가 해제되도록 보장한다.
...
이 기술의 또 다른 이름은 범위 종료로 인해 RAII 객체의 수명이 종료되는 기본 사례의 이름을 따서 SBRM(Scopr-Bound Resource Mangement)이라고 한다.

RAII의 글 인용하여 의역함.

RAII 패턴의 핵심은 리소스가 필요할 때 이미 할당되어 있고 리소스가 필요 없어질 때 객체와 함께 해제되어 객체 내 변수값이 객체와 함께 일정하게 유지되는 클래스 불변성(class invariant)이다

RAII 패턴의 주요 특징은?

동적으로 할당된 메모리가 생성된 범위를 벗어나면 자동으로 해제되는 것이다.

지역 변수는 스택 메모리에 할당되어 범위를 벗어나면 자동으로 해제되지만, 동적 메모리힙 영역에 할당되므로 직접 해제하지 않으면 프로그램이 종료되어도 할당된 상태로 남아 메모리 누수가 발생한다.

스마트 포인터는 지역 변수의 특징과 동적 메모리의 특징을 혼합해서 사용한다. 지역 변수가 생성될 때 동적 메모리를 할당하고 지역 변수가 해제될 때 할당된 동적 메모리를 해제한다.

방법은 의외로 간단하다. 동적 메모리 할당과 해제를 관리하는 클래스를 지역 변수로 만들어서 사용하면 된다. 클래스의 생성자에서 동적 메모리를 할당하고 소멸자에서 메모리를 해제하면 된다.

스마트 포인터 (unique_ptr)

C++13부터 제공되는 unique_ptr은 포인터 객체에 RAII 디자인 패턴을 적용할 수 있는 범용 스마트 포인터 클래스이다. 메모리 관리 객체 또는 래퍼(wrapper)라고 불리는 unique_ptr이중 참조를 허용하지 않고 하나의 포인터 변수만을 허용하는 스마트 포인터이다.

uniquie_ptr은 앞에서 설명한 RAII 디자인 패턴을 구현한 범용 래퍼로서, 메모리 사용 범위를 벗어나면 메모리를 자동으로 해제한다. 스마트 포인터를 사용했을 때와 사용하지 않았을 때 코드를 비교해 보자.

다음 코드는 메모리가 할당되거나 해제되었을 때 화면에 메시지를 출력하는 예이다.

#include <iostream>

using std::cout;
using std::endl;

class class_object {
public:
    class_object() {
        cout << "allocate memory!" << endl;
    }

    ~class_object() {
        cout << "deallocate memory!" << endl;
    }
};


int main()
{
    class_object* unique_pointer = new class_object();

    return 0;
}

실행 결과

allocate memory!

new 키워드로 메모리를 할당하여 객체를 생성했지만, delete로 객체를 소멸하지 않고 프로그램을 종료한다. 메모리 해제를 진행하지 않았으므로 소멸자가 호출되지 않고 프로그램이 종료된다.
즉, class_object 객체에 필요한 만큼의 메모리가 할당된 후 회수되지 않은 것이다.

다음은 똑같은 프로그램을 스마트 포인터 unique_ptr을 이용하는 코드로 수정한 예이다. 이전 코드와 달리 객체를 생성할 때 unique_ptr 클래스로 wrapping 해 주었다. 스마트 포인터로 생성된 unique_pointer는 class_object 객체와 똑같이 사용할 수 있다.
즉, 함수 호출이나 멤버 변수에 접근하는 방법은 같다.

#include <iostream>
#include <memory>

using std::cout;
using std::endl;

class class_object {
public:
    class_object() {
        cout << "allocate memory!" << endl;
    }

    ~class_object() {
        cout << "deallocate memory!" << endl;
    }
};


int main()
{
    std::unique_ptr<class_object> unique_pointer(new class_object());

    return 0;
}

실행 결과

allocate memory!
deallocate memory!

unique_ptr을 생성하는 또 다른 방법으로 make_unique 함수를 사용할 수도 있다. 이때 auto 키워드를 함께 사용하면 코드를 간결하게 작성할 수 있다. 무엇보다 new 연산자를 사용하지 않으므로 포인터를 사용하는 복잡함이 덜하다.

auto unique_pointer = std::make_unique<class_object>();

RAII 패턴은 메모리를 필요한 범위에서 사용하고 범위를 벗어나면 자동으로 해제한다. 따라서 개발자가 객체의 생명 주기를 직접 관리하면서도 메모리 관리에 많은 신경을 쓰지 않아도 된다는 장점이 있다.


Unreal C++ 스마트 포인터

0개의 댓글