C++
의 대표적인 특징은 메모리를 직접 관리할 수 있다는 점이다. 즉, 필요할 때 메모리를 할당하고 필요하지 않을 때 메모리를 해제할 수 있다. 덕분에 C++
언어로 만든 프로그램은 메모리를 효율적으로 사용해 성능이 좋지만, 메모리 할당과 해제 등을 개발자가 직접 관리해야 하므로 잘못하면 메모리 누수
같은 문제를 야기할 수도 이싿. 이처럼 C++
언어의 메모리 관리 기능은 양날의 검이라고 할 수 있다.
최근 프로그래밍 언어들은 메모리 관리가 아주 쉽거나 아예 필요 없는 언어도 많다. C++
언어도 메모리 관리를 지원하고자 auto_ptr
을 제공했지만, 여러 가지 문제로 C++17
부터는 제외되었다.
모던 C++
에서는 auto_ptr
을 대신해 unique_ptr
, shared_ptr
, weak_ptr
등 다양한 스마트 포인터(smart pointer)
를 제공한다.
스마트 포인터
를 이해하려면 RAII라는 디자인 패턴
을 알아야 한다. RAII
는 Resource Acquisition Is Initialization
의 앞 글자를 따서 만든 단어로, 리소스 할당은 초기화다
라고 직역할 수 있다. 이를 cppreference.com에서 이해하기 쉽게 해설한 내용을 인용하면 다음과 같다.
RAII 패턴은 객체에 접근할 수 있는 모든 곳에서 리소스(메모리, 파일, 식별자 등)를 사용하고자 할 때 항상 사용할 수 있음을 보장한다(리소스의 가용성은 클래스 불변성이기 때무넹 리소스를 사용할 수 있는지 매번 확인해 볼 필요가 없음).
또한 제어 객체의 수명이 끝아면 획득 순서의 역순으로 모든 리소스가 해제되도록 보장한다.
...
이 기술의 또 다른 이름은 범위 종료로 인해 RAII 객체의 수명이 종료되는 기본 사례의 이름을 따서SBRM(Scopr-Bound Resource Mangement)
이라고 한다.RAII의 글 인용하여 의역함.
RAII
패턴의 핵심은 리소스가 필요할 때 이미 할당되어 있고 리소스가 필요 없어질 때 객체와 함께 해제되어 객체 내 변수값이 객체와 함께 일정하게 유지되는 클래스 불변성(class invariant)
이다
RAII 패턴의 주요 특징은?
동적으로 할당된 메모리가 생성된 범위를 벗어나면 자동으로 해제되는 것이다.
지역 변수는 스택 메모리
에 할당되어 범위를 벗어나면 자동으로 해제되지만, 동적 메모리
는 힙 영역
에 할당되므로 직접 해제하지 않으면 프로그램이 종료되어도 할당된 상태로 남아 메모리 누수
가 발생한다.
스마트 포인터
는 지역 변수의 특징과 동적 메모리의 특징을 혼합해서 사용한다. 지역 변수가 생성될 때 동적 메모리를 할당하고 지역 변수가 해제될 때 할당된 동적 메모리를 해제한다.
방법은 의외로 간단하다. 동적 메모리 할당과 해제를 관리하는 클래스를 지역 변수로 만들어서 사용하면 된다. 클래스의 생성자에서 동적 메모리를 할당하고 소멸자에서 메모리를 해제하면 된다.
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
패턴은 메모리를 필요한 범위에서 사용하고 범위를 벗어나면 자동으로 해제한다. 따라서 개발자가 객체의 생명 주기를 직접 관리하면서도 메모리 관리에 많은 신경을 쓰지 않아도 된다는 장점이 있다.