메모리 관련

김민오·2022년 7월 9일
0

Microsoft Docs를 참고함

https://docs.microsoft.com/ko-kr/cpp/mfc/memory-management?view=msvc-170

https://docs.microsoft.com/ko-kr/cpp/cpp/pointers-cpp?view=msvc-170

메모리 관리

Stack Frame

스택 프레임이란 함수에 전달되는 파라메터나 함수 내부에서 로컬로 정의되어 있는 변수들을 일시적으로 보유하는 공간(메모리 영역)이다. 컴파일러가 자동으로 공간을 할당해준다.

프레임에 메모리가 할당되는 경우 함수 내부의 지역 변수를 정의할 때 데이터의 크기가 매우 큰 경우에도 이 변수를 보유할 수 있는 공간이 스택 프레임에 할당된다. 또한 이 프레임에 할당된 변수들은 범위(Scope)를 벗어날 때 자동으로 삭제된다(소멸자는 메모리가 회수되기 전에 자동으로 호출된다).

void Function()
{
	// Local 변수가 스택 프레임에 할당된다.
	int Data;
    // 이 영역을 벗어나면(=함수가 종료되는 것으로) 
    // 프레임에 할당된 변수는 삭제된다.
}

이렇게 프레임에 메모리를 할당하는 방법으로 얻을 수 있는 메리트는, 할당한 개체가 Scope를 벗어나면 자동으로 삭제된다는 것이다. 즉 메모리를 할당하고 해제하는 부분에 대해서 신경을 쓸 필요가 없다는 뜻이다.

다만 이렇게 프레임에 할당한 메모리는 범위를 벗어나는 외부에서는 사용할 수가 없기 때문에 본인이 원하는 값을 범위 밖에서도 사용을 하고 싶다면 아래에서 후술할 힙 메모리를 사용해야한다.

또한 처리해야할 데이터가 매우 큰 경우에도, 스택보다는 힙에서 사용하는것이 권장된다.

Heap

https://en.wikipedia.org/wiki/Heap

힙 영역은 프로그램이 메모리를 얼마나 사용할 것인지에 따라 예약되어 있다. 프로그램 코드와 스택을 제외한 영역이며 위키에서는 동적할당을 위한 메모리 영역으로 설명되어 있다.

Raw pointer

포인터는 값의 주소를 메모리에 저장하고 이 값에 접근하는데에 사용하는 변수의 형식이다.

Raw Pointer의 경우 수명이 제어되지않는 포인터인데, 사용자가 수동으로 메모리를 할당하고 해제를 해줘야하는 포인터이다.

참고로 사용자가(프로그램이) Heap 메모리 영역에 값을 할당할 경우 포인터의 형태로 값의 주소를 받게된다.

앞서 언급했듯이 수명이 제어되지않아 프로그래머가 수동으로 관리해야하기에 값이 더이상 사용되지 않을 것으로 보인다면 힙에 할당된 값을 해제해야한다.

만약 할당한 값을 해제하지않는다면 메모리 누수가 발생하여 다른 프로그램이 해당 메모리 영역을 사용할 수 없게된다.(메모리를 해제하지 않아 계속 점유중이니까)

RAII(Resource acquisition is initialization)

https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

https://en.cppreference.com/w/cpp/language/raii

앞서 메모리 수동해제에 대해서 언급을 했는데 최근에 나오는 프로그래밍 언어들 대부분은 가비지 콜렉터와 같이 자동으로 메모리를 관리하는 기능이 탑재되어 있다.

그럼 C/C++같은 언어들이 이상한거아니냐? 구린거 아니냐? 그런 생각이 들수도 있겠지만 메모리를 수동으로 관리해서 얻을 수 있는 이점도 있고 자동으로 관리해서 얻을 수 있는 이점 또한 존재하기에 케이스 바이 케이스.. 라고 생각한다.

다만 C++언어를 오랜기간 사용하던 매우 똑똑한 개발자들도 이런 메모리를 수동으로 관리하면서 생기는 이슈들에 대해서 못본척을 하지는 않았기때문에, 메모리가 Unmanaged한 상황에서는 값들의 수명을 어떻게 관리할 것인가에 대한 고민을 많이 하게되었고 RAII는 그런 고민끝에 나온 원칙이라고 볼 수 있다.

RAII는 Resource acquisition is initialization 약자이다 해석하면 "리소스의 획득은 초기화입니다"인데 그냥 봐서는 무슨 의미인지 알 수가 없다.

우선 앞서 말한 RAII는 잠깐만 잊고 메모리를 수동으로 관리한다는 측면만 살펴보자, 메모리를 수동으로 관리한다면 할당과 해제도 사용자가 직접 해줘야한다.

void SomeFunction()
{
	SomeClass sc = new SomeClass();
}

위와 같이 new 키워드를 사용하여 값을 메모리에 할당한다면

void SomeFunction()
{
	SomeClass sc = new SomeClass();
    
    // Other Logic . . . .
    
    delete sc;
}

이렇게 해제해야한다. 이 정도는 크게 문제될게없다, 그러나 대부분의 개발에서 작성되는 함수들은 당연히 위와같이 두 세줄 남짓한 간단한 코드로 구성되지않고

void SomeFunction()
{
	SomeClass sc = new SomeClass();
    
    // 값은 당연히 한 두개가 아니라 여러개를 쓸 수도 있고
    // 예외도 처리해야 할거고
    // 필요에 따라서는 다른 함수를 호출하거나..
    // 여러 케이스가 존재할 수가 있다.
    
    delete sc;
}

자원을 해제하기 전에 예외를 처리받아 함수가 종료될 수도 있고 자원이 해제되어야 하는 타이밍에 엉뚱한 함수가 호출될 수도 있다.

물론 C/C++이 메모리를 수동으로 관리할 수 있는 강력한 권한을 개발자에게 준 만큼 당연히 책임은 코드를 거지같이 작성한 개발자에게 돌려져야하겠지만... 구조가 복잡한 프로젝트나 혹은 지속적으로 개발/확장을 해야하는 경우 전적으로 개발자가 메모리를 수동으로 할당, 해제하면서 발생하는 문제점들을 등한시할 수는 없기 때문이다.

CPP Reference에서는 RAII를 아래와 같이 요약하고 있다.

자원을 클래스로 캡슐화하고 자원을 사용할 때는 클래스의 로컬 인스턴스를 통해 사용하며, 값이 범위를 벗어나면 리소스가 자동으로 해제되는 것이라고 보면 되겠다.

후술할 스마트 포인터는 이 RAII 개념이 적용된 포인터라고 보면 되겠다.

Smart Pointer

https://docs.microsoft.com/ko-kr/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170

아래의 코드는 Microsoft Document의 예제이다.

스마트 포인터는 스택 영역에서 선언 되고, 힙 영역에 할당한 값을 가리키는 Raw Pointer를 사용하여 초기화한다. 따라서 스마트 포인터가 초기화 된다는 것은 Raw Pointer를 소유하는 것임을 알 수 있다.

이 말은 Raw Pointer가 지정한 메모리를 스마트 포인터가 삭제해야한다는 것인데, 앞에서 언급한 스택 프레임을 다시 생각해보자 스택 프레임은 영역을 벗어나면 메모리에서 해제된다. 만약 스마트 포인터의 소멸자에 소유한 Raw Pointer를 해제할 호출이 포함된다면 스택 프레임 영역에서 스마트 포인터가 범위를 벗어나는 시점에서 소멸자가 호출되고, 스마트 포인터의 소멸자는 Raw Pointer를 해제하기 때문에 RAII를 지킬 수 있게 되는 것이다.

스마트 포인터는 아래와 같이 3가지 종류가 있다.

unique_ptr

하나의 소유자만 허용하는 포인터이다, 소유자를 변경할 수는 있지만 이를 복사하거나 공유하는 것은 불가능하다. 크기가 작아 효율적이다.
make_unique()함수를 통해서 인스턴스를 만드는 것이 가능하고, std::move() 함수를 사용하여 소유권을 이동할 수 있다.

// 인스턴스 생성.
auto actor = make_unique<actor>("Mino", "wizard");
vector<string> actor_names = { actor->name };

// actor의 소유권을 actor2로 이전한다.
unique_ptr<actor> actor2 = std::move(actor)

shared_ptr

참조 횟수가 계산되는 스마트 포인터이다(참조 카운트). 포인터 하나를 여러 소유자에게 할당하고자 할 때 사용할 수 있다. 이 포인터는 모든 shared_ptr 소유자가 범위를 벗어나거나 소유권을 해제할 때까지 삭제되지 않는다. 2개의 포인터로 구성되어 있는데 하나는 개체를 가리키고 또 다른 하나는 참조 횟수가 포함되어 공유 제어 기능을 동작하기 위해 사용된다.


https://docs.microsoft.com/ko-kr/cpp/cpp/how-to-create-and-use-shared-ptr-instances?view=msvc-170

위의 그림은 shared_ptr이 어떻게 동작하는지를 잘 보여주고 있는데, 개체를 가리키는 shared_ptr이 늘어날 때마다 공유 제어 포인터가 가리키는 제어 블로그이 참조 카운트가 1씩 증가하는 것을 볼 수 있다.

생성 방법은 unique_ptr과 크게 다르지않다.
auto cinema_1 = make_shared<Movie>("TOP GUN", "Action");

shared_ptr<Song> cinema_2(new Song("Thor4", "Hero"));

// 생성자 호출 시, 복사해서 사용하는 것이 가능하다.
// 이 경우 참조 카운트가 1 증가한다.
auto cinema_3(cinema_2)

// 이렇게도 사용할 수 있다.
auto cinema_4 = cinema_1

weak_ptr

shared_ptr과 같이 사용가능한 경우의 스마트 포인터인데, shapred_ptr 인스턴스가 소유하는 개체에 대해서 접근할 수는 있지만 참조 카운트에는 참가하지 않는다.

profile
https://github.com/gimhema, https://gimhema.tistory.com/

0개의 댓글