프로그래밍 언어/C & C++ 정리

<memory> smart pointer

게임 개발 2023. 10. 29. 11:54

 

 

<memory> smart pointer



 스마트 포인터는 <memory> 헤더 파일의 std namespace에 정의된다.

 

smart pointer는 포인터처럼 행동하는 클래스 객체이지만, 몇 가지 추가 기능을 지닌다.

이번 게시물에서는

예시를 통해 동적 메모리 대입을 관리하기 위한 스마트 포인터 템플릿에 대하여 알아볼 것이다.

 

가령 어떠한 함수가 원시 포인터를 사용한다고 가정하면,

마지막에 delete 구문을 빼먹었거나, 혹은 delete 구문 전에 오류가 나면

대입된 메모리가 해제되지 못한다.

 

지역 변수는 스택 메모리부터 삭제된다.

그래서 포인터가 차지하고 있던 메모리가 해제되고 이때,

포인터가 지시하는 메모리도 함께 해제된다면 좋을 것이다.

 

이러한 과정을 거치기 위해서는

포인터의 수명이 다했을 때 프로그램이 어떤 추가 조치를 취해야 한다는 것을 의미한다.

 

이러한 추가 서비스는 기본 데이터형들에 대해서는 제공되지않고,

소멸자 매커니즘을 통해서 클래스들에 대해서만 제공된다.

따라서 가령 string * ps 라는 객체가 일반 포인터인 것이 문제가 된다면,

ps를 클래스 객체로 만들어

ps의 수명이 다했을 때 메모리도 파괴자를 통해서 함께 해제될 수 있다. 

이것어 auto_ptr의 탄생 배경이다.

 

 

auto_ptr



 auto_ptr은 스마트 포인터의 시초이다.

auto_ptr은 C++11 부터 사용 중지 권고, C++17 부터는 제거되었다.

C++03에 이미 auto_ptr라는 스마트 포인터가 있었지만, 실패한 시도로 간주되었다.

auto_ptr 이 나온 그 당시에는

C++ 언어 자체가 아직 스마트 포인터를 제대로 구현할 준비도 되지 않았었다.

 

auto_ptr의 소멸자는

~auto_ptr()
{
	delete _Myptr;
}

이런식으로 구현되어있는데, 배열 단위의 해제는 지원해주지 않는다.

또한 값 단위 복사를 할 수 없기 때문에 단지 소유권 이동이 전부이다.

이러한 상황 때문에 auto_ptr 보다 auto_ptr을 대처할 unique_ptr이 등장한다.

 

 

 

유일 포인터 unique_ptr



 

 unique_ptr라는 이름은대상 객체에 대한 유일 소유권(unique ownership)을 나타낸다.

이는, 서로 다른 두 unique_ptr가  같은 객체를 가리키는 일이 없다는 것이다.

참조와 역참조 등의 사용법 자체는 보통의 포인터(원시 포인터)와 다를 바는 없다.

 

#incldue <memory>

int main()
{
	unique_ptr<double> dp{new double};
    *dp = 7;
    . . .
    cout << "Tht value of *dp is" << *dp << endl;
}

 

원시 포인터와 주된 차이점은, unique_ptr는 범위를 벗어날 때 해당 메모리가 자동으로 해제된다는 것이다.

이 때문에, 동적으로 할당하지 않는 주소로 unique_ptr를 초기화해서는 안된다.

 

<예시>

 

double d = 7.2;
unique_ptr<double> dd{&d}; // 버그 : 메모리 블록이 잘못 해제됨

 

해당 예시에서 포인터 dd의 소멸자는 d를 해제하려고 한다.

 

 메모리의 유일 소유권을 보장하기 위해, unique_ptr는 복사를 허용하지 않는다.

unique_ptr<double> dp2{dp}; // 오류 : 복사 불가
dp2 = dp;					// 마찬가지임

 

그렇지만 메모리 주소를 다른 unique_ptr에 넘겨주는 것이 가능하다.

 

unique_ptr<double> dp2{move(dp)}, dp3;
dp3 = move(dp2);

 

해당 예시에너는 dp가 원래 가리키던 메모리 블록에 대한 소유권은 dp에서 dp2로 넘어가고,

그 다음에 다음 dp3로 넘어간다.

소유권을 넘겨준 dp와 dp2는 nullptr가 되므로 해당 소멸자는 아무것도 해제하지 않는다.

dp3의 소멸자는 dp3이 소유한 메모리 블록을 해제한다.

함수가 unique_ptr를 돌려줄 때도 마찬가지 방식으로 소유권이 전달된다.

 

다음 예에서는 dp3은 f()안에서 할당된 메모리 블록을 넘겨받는다.

 

std::unique_ptr<double> f()
{	return std::unique_ptr<double>{new double}; }

int main()
{
    unique_ptr<double> dp3;
    dp3 = f();
}

 

해당 예제에서 move()는 필요하지 않다.

이 함수의 반환값은 임시 객체이며, 임시 객체에는 이동 의미론이 적용되기 때문이다.

 

 auto_ptr이 배열을 처리하지 못한 것과는 달리,

unique_ptr은 배열에 대해 특수화되어 있다. 배열은 delete[]로 해제해야하므로 특수화가 필요하다.

또한, 이 특수화는 보통의 배열과 같은 방식으료 요소들에 접근하는 수단도 제공한다.

 

unique_ptr<double[]> da{new double[3]};
for (unsigned i = 0; i < 3; ++i)
	da[i] = i+2;

대신 배열 unique_ptr 에 대해서는 operator*를 사용할 수 없다.

원시 포인터 포인터 연산에 대한 것.

 

 

공유 포인터 shared_ptr

 

 

 공유 포인터 (shared pointer)라는 명칭에서 짐작하겠지만,

shared_ptr은 프로그램의 여러 부분이 공유하는 메모리를 관리하는데 쓰인다.

각 부분이 같은 메모리 블록에 대한 포인터를 각자 가지는 형태로 말이다.

공유되는 메모리 블록은 그것을 가리키는 마지막 shared_ptr가 소멸할 때 해체된다.

이러한 메모리 공유 능력을 이용하면 프로그램이 대단히 단순해진다.

프로그램이 복잡한 자료 구조들을 사용한다면 더욱 그렇다.

특히, 공유 포인터는 동시성 구현 시 극히 중요하다.

 

다수의 스레드가 같은 메모리 블록에 접근하며,

그 스레드들이 모두 종료되면

메모리 블록이 자동으로 해제되어야 하는 상황을 생각하면 이해가 될 것이다.

unique_ptr과 달리 shared_ptr은 복사를 허용한다.

공유를 위한 스마트 포인터이므로 당연한 일이다.

 

 가능하다면 new를 사용하지 말고

make_shared 함수로 shared_ptr을 생성하는 것이 바람직하다.

 

 

 

 

약한 포인터 weak_ptr

 

 

 공유 포인터와 관련된 문제점으로,

공유 포인터들의 참조 관계가 꼬여서 순환 참조(cyclic reference)가 발생하면

메모리 블록이 해제하지 못한다.

이런 순환 고리를 깨는 수단이 weak_ptr이다.

weak_ptr는 메모리 블록(공유된 것이든 아니든)에 대한 소유권을 주장하지 않는다.

 

 

 

 

 

해당 게시글은

Discovering modern c++ 2/e 서적과 microsoft 공식 홈페이지를 참고하여 제작되었습니다.

 

 

'프로그래밍 언어 > C & C++ 정리' 카테고리의 다른 글

C++ 오류 처리  (0) 2023.11.17
effective c++  (0) 2023.11.05
volatile  (0) 2023.10.29
포인터  (0) 2023.10.28
부동소수점  (0) 2023.06.20