C++ 스마트 포인터(smart pointer)

프로그래밍/C++ 2019.11.06 댓글 Plorence

smart pointer는 포인터처럼 행동하는 클래스 객체입니다.

스마트 포인터는 동적 메모리를 관리하기 위해 있는데 포인터 변수이 스택에서 해제된다면 지시하는 메모리 블럭도 같이 할당 해제가 됩니다.

 

스마트 포인터는 왜 필요한가?

void test() {
       std::string * str = new std::string("Hello!!");
       //작업후에
       if (str->length() == 0) {
              throw std::exception();
       }
       delete str;
       return;
}

해당 함수는 예외가 발생하면 delete 구문은 실행할 수 없습니다.

(예외가 발생할 수 없는 함수지만, 발생한다고 가정해봅시다.)

그래서 포인터 변수 str가 가리키는 메모리 블록은 할당 해제하지 않습니다.

결국엔 memory leak(메모리 누수)가 발생하게 됩니다.

포인터 변수(str)가 스택에서 해제될 때 지시하는 메모리 블럭도 같이 할당 해제된다면 좋겠는데.. 싶어서 나온 게 스마트 포인터입니다.

스마트 포인터도 결국엔 객체이기 때문에 수명이 다했을 때 파괴자를 통해서 지시하는 메모리 블럭이 할당 해제가 됩니다.

 

스마트 포인터 사용

먼저 스마트 포인터를 사용하려면 memory 헤더 파일을 포함해야 합니다.(namespace: std)

스마트 포인터는 총 3개가 있습니다. (weak_ptr라는 스마트 포인터도 있지만, 생략함.)

  • auto_ptr

  • unique_ptr

  • shared_ptr

3개의 스마트 포인터들은 각각 new를 통해 얻어지는 주소를 대입할 포인터를 정의합니다.

문법은 셋 다 동일하고 스마트 포인터가 수명을 다했을 때 파괴자는 delete를 사용하여 메모리를 해제한다는 점도 동일합니다.

다만 auto_ptr는 C++11 이후로 없어졌고 unique_ptr, shared_ptr가 있습니다.

(visual studio 2017 최신 버전을 사용하고 계시다면 디폴트 컴파일러는 C++14)

그리고 반드시 자동으로 할당 해제하려는 메모리가 heap에 존재해야 합니다.

std::auto_ptr<std::string> str1(new std::string("Hello!!"));
std::unique_ptr<std::string> str2(new std::string("Hello!!"));
std::shared_ptr<std::string> str3(new std::string("Hello!!"));

문법은 셋다 똑같습니다.

 

test함수를 스마트 포인터로 고치면 다음처럼 됩니다.

void test() {
       //std::string * str = new std::string("Hello!!");
       std::auto_ptr<std::string> str(new std::string("Hello!!")); //string을  가리키는 auto_ptr
       //작업후에
       if (str->length() == 0) {
              throw std::exception();
       }
       //delete str;
       return;
}

 

형변환

int main(void) {
       std::unique_ptr<int> up;
       int * var = new int(100);
       up = var; //에러! 변환 생성자로 동작하지 않음
       up = std::unique_ptr<int>(var); //명시적 변환(허용)
       std::unique_ptr<int> up2(var); //명시적 변환(허용)
}

암시적 변환은 안되고 명시적 변환만 가능합니다.

 

스마트 포인터 고려 사항

스마트 포인터인 auto_ptr, unique_ptr에는 소유권 개념이 있습니다.

즉 둘 이상이 같은 메모리 블록(객체)을 지시할 수 없습니다.

int * var = new int(100);
std::auto_ptr<int> up = std::auto_ptr<int>(var); //이 구문이 실행되면  소유권은 up가 가지고 있음
std::auto_ptr<int> up2 = up; //대입을 통해 소유권이 up2으로 이전됨

소유권이 이전되면 스마트 포인터 up은 널포인터를 가집니다.

이러한 소유권 개념이 있는 이유는 소유권 개념이 없으면 같은 객체에 대해 두 번 해제하기 때문입니다.

(up의 수명이 해제될 때 한번, up2의 수명이 해제될때 한번)

 

이 문제(소유권)를 해결하고 싶으면 객체를 복사해서 서로 다른 객체를 지시하게 해야 합니다.

 

unique_ptr 객체에서 소유권 이전을 하고 싶다면, std::move함수를 사용하면 됩니다.

std::unique_ptr<int> up;
int* var = new int(100);
up = std::unique_ptr<int>(var);
std::unique_ptr<int> up2 = std::move(up);

unique_ptr클래스는 따로 소유권 이전을 도와주는 생성자, 멤버 함수가 없으므로 이렇게 해야 합니다.

 

shared_ptr

는 소유권 대신 하나의 특정한 객체를 참조하는 스마트 포인터들이 몇 개인지 추적합니다.

이것을 참조 카운팅(reference counting)이라고 부르고 대입할 때마다 1씩 증가, 수명이 다할 때마다 1씩 감소됩니다.

마지막 스마트 포인터의 수명이 다했을 때(참조 카운트가 0일 때) delete가 호출됩니다.

int * var = new int(100);
std::shared_ptr<int> up = std::shared_ptr<int>(var); //참조 카운트 1
std::shared_ptr<int> up2 = up; //참조 카운트 2

 

auto_ptr보다 unique_pr가 더 좋은 이유

auto_ptr가 C++11 이후로는 없어진 것도 있지만 일반적인 소유권 이전은 컴파일 에러가 발생합니다.

int * var = new int(100);
std::unique_ptr<int> up = std::unique_ptr<int>(var);
std::unique_ptr<int> up2 = up; //컴파일 에러
       
int * var2 = new int(100);
std::auto_ptr<int> ap = std::auto_ptr<int>(var2);
std::auto_ptr<int> ap2 = ap; //문제 없음

그래서 아래와 같은 방법으로 사용해야 합니다.

int * var = new int(100);
std::unique_ptr<int> up = std::unique_ptr<int>(var);
std::unique_ptr<int> up2 = std::unique_ptr<int>(new int(*up)); //동적할당하고 100으로 초기화함
       
std::string * str = new std::string("Hello");
std::unique_ptr<std::string> up_str = std::unique_ptr<std::string>(str);
std::unique_ptr<std::string> up_str2 = std::unique_ptr<std::string>(new  std::string(*str));

이러한 이유로 unique_ptr는 auto_ptr보다 안전합니다.

그리고 unique_ptr을 다른 unique_ptr로 대입할 수 있게 도와주는 std::move라는 표준 라이브러리 함수가 있습니다.

std::unique_ptr<int> up = std::unique_ptr<int>(var);
std::unique_ptr<int> up2 = std::move(up);

move함수를 통해 재사용한다면 up은 널포인터가 됩니다.

그리고 auto_ptr에는 배열을 사용할 수 없지만 unique_ptr은 배열 사용이 가능합니다.

std::unique_ptr<int[]> up = std::unique_ptr<int[]>(new int[1000]);

 

스마트 포인터 선택

하나의 객체에 대해 하나 이상의 포인터를 사용한다면, shared_ptr을 선택해야 합니다.

만약 unique_ptr이었다면 하나라도 수명이 다했을 경우에 객체가 소멸하게 됩니다.

컴파일러가 shared_ptr을 지원하지 않는다면, boost 라이브러리에서 버전을 다운로드할 수 있습니다.

 

프로그램이 같은 객체를 가리키기 위해 다중 포인터를 필요로 하지 않는다면, unique_ptr을 사용해야 합니다.

 

댓글