kwan's note

메모리 관리와 스마트 포인터 본문

Computer Programming/c++ programming

메모리 관리와 스마트 포인터

kwan's note 2022. 7. 18. 14:45
반응형

메모리 기본 작동과정

int i = 7;

위와같은 변수 i는 스택에 저장되고

아래와 같은 변수 ptr가 가르키는 값은 힙에 저장된다.

스택에 할당된 변수는 scope를 벗어나면 자동으로 해제가 되는 반면 힙에 저장된 변수는 해제시켜주어야 한다.

int* ptr = new int;

 

물론 이때 ptr은 스택에 저장된다. 포인터 역시 변수의 일종이기 때문이다.

 

malloc free -> new delete

 

c에선 malloc 을 통해 메모리를 할당하였다.

 

여전히 c++에서도 malloc을 사용할 수 있음에도

거의 동일한 작업을 하는 new 가 생긴 이유에 대해서 궁금할 수 있을텐데

 

가장 큰 차이는 사용성에 있다고 볼 수 있다.

malloc은 힙영영에 메모리를 따로 빼놓을뿐 어떠한 객체에 이용되는지 알 지 못한다. 또한 그 크기도 sizeof 등을 이용해 지정해주어야 했다. 하지만 new 키워드를 사용하면 적절한 크기의 메모리 공간을 할당하고 객체의 생성자를 호출해 객체를 생성한다.

 

이와 동일하게 free도 메모리를 해제할 뿐 소멸자를 호출하지 않는 반면 delete는 소멸자를 호출하여 객체를 제거한다.

 

이에따라 C++에서는 일반적인 경우 new delete를 사용하는것이 더 편하거나 안전하므로 malloc free 보다는 new delete를 사용하도록 한다.

 

배열의 삭제

 

Simple이라는 클래스가 존재하고 다음과 같은 객체 배열을 생성했다고 해보자.

Simple* mySimpleArray = new Simple[4];

 

이를 삭제하려면 delete Simple이 아닌 delete[] Simple을 사용해야 한다.

delete를 사용하면 컴파일러에 따라 정상적을 동작할 수 도 있는데 오히려 더 위험할 수 있다.

어떤 컴파일러는 객체를 가르키는 포인터만 삭제한다고 생각하여 첫번째 원소만 소멸자를 호출할 수 있고 때로는 아예 메모리 손상이 발생할 수 있다.

 

따라서 new로 생성하면 delete로 new[]로 생성하면 delete[]로 해제해야 한다.

 

또, 당연히 포인터 객체에 대한 배열에서는 모든 원소에 대해 해제를 해주어야 한다.

 

 

다차원 동적 할당 배열

실행시간(runtime)에 차원수를 결정하고자 한다면 힙 배열로 생성한다. 하지만 다음과 같이 작성하면 문제가 발생한다.

char** map = new map[a][b];

 

힙에서는 메모리 공간이 연속적으로 할당되지 않으므로 일차원 배열을 할당하는것을 반복하는 방식으로 할당해야 한다.

 

char** allocateCharMap(size_t row, size_t col)
{
	char** map = new char*[row];
    for(size_t i =0;i<row;i++)
    {
    	char[i]= new char[col];
    }
    
    return map;
}

이와 동일하게 해제할 때도 하위배열부터 하나씩 삭제해야 한다.

 

가비지 컬렉션

자바나 C#과 같은 매니지드 언어를 사용하던 사람들은 알고있을 가비지 컬렉션은 메모리를 직접 해제하지 않더라도 일정 기간 이상 사용하지 않은 메모리를 해제해준다. 이는 shared_ptr등의 스마트 포인터와 동작 아이디어 자체는 비슷하다.

 

가비지 컬렉션 구현기법에는 여러가지가 있는데 그중에 하나는 mark and sweep이다. 이 방식은 프로그램에 있는 모든 프로그램에 있는 모든 포인터를 검사한 후 참조하는 메모리를 사용하고 있는지 확인한다. 한 주기가 끝난 시점에서도 사용하지 않는 메모리는 더이상 사용하지 않는것으로 판단하여 해제한다.

 

C++에서 이를 구현하려면 다음과 같은 방식으로 할 수 있다.

1. 모든 포인터를 쉽게 탐색하도록 포인터를 가비지 컬렉터에 리스트로 등록한다.

2. 가비지 컬렉터가 객체의 사용상태를 표시할 수 있도록 모든 객체가 가비지 관리 클래스를 상속하도록 한다.

3. 객체에 동시에 접근을 못하도록 가비지 컬렉터가 작동하는동안 포인터를 변경할 수 없게 한다.

 

다만 가비지 컬렉터의 단점도 있는데 먼저 가비지 컬렉터가 동작하는동안 프로그램의 성능이 크게 하락한다. 다음으로 가비지 컬렉터가 삭제를 하므로 소멸자가 원하지 않는 상황에 호풀될 수 있다.

 

이러한 가비지 컬렉터의 단점을 보안하기 위한 방법으론 객체 풀링 방식이 있다.

 

스마트 포인터

 

마지막으로 다룰 내용은 스마트 포인터이다. C++에서 스마트 포인터는 매우 중요한 개념이다.

C++에서 메모리 관리는 에러나 버그가 일어나는 가장 흔한 요소이기 때문인데 스마트 포인터는 동적으로 포인터를 할당하는 기존 방식의 단점을 많이 해소해 주기 때문에 그만큼 중요하고 잘 알아야할 필요성이 있다.

 

기본적으로 스마트 포인터는 스코프를 벗어나면 자동으로 할당된 리소스를 해제한다.

 

unique_ptr 

가장 먼저 unique_ptr 은 단독 소유권 방식을 지원한다. 포인터를 깜빡하고 해제하는것을 잊는것 뿐 아니라 여러 객체가 포인터를 가지고 있다면 마지막으로 사용한곳에서 해제를 해야하는데 마지막에서 사용하는곳을 특정하기 어렵기 때문에 unique_ptr은 소유권을 단독으로만 가질 수 있다.

 

C++14부터는 make_unique 라는 함수를 제공하므로

 

unique_ptr<Simple> mySmartptr = make_unique<Simple>();

 

의 형태로 unique pointer를 생성할 수 있다.

C++14이전의 컴파일러에서는

 

unique_ptr<Simple> mySmartptr(new Simple());

의 형태로 unique_ptr를 생성할 수 있다.

 

가독성을 위해서는 make_unique를 사용하는것이 좋고 c++17이전에서는 make_unique를 사용하지 않는다면 생성하는 객체의 생성자에서 exception이 발생한 경우 메모리 누수가 발생할 수 있었다.

 

unique_ptr은 복사생성자를 의도적으로 제거함으로서 복사할 수 없게 만들어졌다. 만약 소유권을 이동하고 싶다면 std::move를 통해 이동시킨다.

    //unique_ptr 클래스의 구현 중 다음과 같은 방식으로 복사생성자, 복사대입생성자를 삭제하였다.
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

shared_ptr

 

포인터를 깜빡하고 해제하는것을 잊는것 뿐 아니라 여러 객체가 포인터를 가지고 있다면 마지막으로 사용한곳에서 해제를 해야하는데 마지막에서 사용하는곳을 특정하기 어렵기 때문에 리소스 소유자를 추적하도록 reference counting(참조 횟수 카운팅, 레퍼런스 카운팅)을 통해 더이상 참조하는 객체가 없으면 그때 해제되는 메모리 형태이다.

 

레퍼런스 카운팅은 클래스의 인스턴스 수나 사용중인 객체를 추적하는 메커니즘이다. 이를통해 중복삭제가 발생하지 않게 관리한다.

 

shared_ptr의 사용법은 unique_ptr과 비슷하다. 생성방식은 make_shared()이다. 또한 unique_ptr과 마찬가지로 get, reset 메서드도 제공한다. 다만 release의 경우 다른곳에서 사용할 수 있으므로 제공하지 않는다.

 

weak_ptr

 weak_ptr은 shared_ptr 가 가르키는 리소스의 레퍼런스를 관리한다. weak_ptr은 리소스를 직접 소유하지 않는다.  weak_ptr이 삭제될 때 가르키던리소스를 삭제하지 않고 shared_ptr가 리소스를 해제했는지 알아낸다.

 

이러한 weak_ptr를 사용하는 이유는 shared_ptr가 순환참조에 빠지는것을 막기 위해서이다.

shared_ptr은 레퍼런스 카운팅 기반이기에 순환 참조에 대한 잠재적인 문제가 있을 수 있다. 즉, A와 B가 서로에 대한 shared_ptr을 들고 있으면 레퍼런스 카운트가 0이 되지 않아 메모리가 해제되지 않는다. 따라서 A에 대해 B가 shared_ptr를 통해 가지고 있지 않고 weak_ptr를 통해 참조한다면 이러한 순환참조 문제를 회피할 수 있다.

 

반응형