본문 바로가기
C,C++/C++

[C++] <memory> std::allocator<T> 클래스

by woohyeon 2019. 12. 12.
반응형

안녕하세요.

오늘은 <memory> 헤더의 allocator<T> 클래스에 대해 알아보겠습니다.
allocator는 유연한 메모리 관리를 위한 클래스로 할당자라고도 합니다.

일반적으로 c++에서 메모리를 동적으로 할당하고 해제할 때 new/delete 연산자를 사용하는데
allocator 클래스는 주로 라이브러리를 작성할 때, 특히  표준 라이브러리의 컨테이너를 구현할 때 많이 사용됩니다. 
할당자는 fine-grained 방식 즉,  메모리 관리를 좀 더 세밀하게 컨트롤 해야하고 유연하고 효율적으로 사용해야 할 경우에 유저가 원하는 메모리 할당 방식으로 구현할 수 있습니다. 즉 allocator 클래스를 상속받아 멤버 함수를 override해서 커스텀할 수 있습니다.

컨테이너는 메모리를 최대한 효율적으로 관리할 수 있어야 하는데 new/delete 연산자로는 세밀한 메모리 관리 능력이 떨어집니다. 컨테이너는 주로 데이터를 저장하고 관리하는 공간입니다. 따라서 저장 공간의 생성, 확장, 추가, 삭제 등이 빈번하게 발생합니다. 만약 이 과정에서 new/delete 연산자를 사용할 경우 다음과 같은 문제점이 있습니다.

new 연산자를 사용하게 되면 따라오는 조건이 3가지가 있습니다. ( 클래스 타입이라는 가정하에 )
1. 기본 생성자 필요
2. 메모리 할당
3. 모든 요소 초기화

위 3가지 조건이 세트로 따라오게 되는데
이는 요소가 많아질수록 컴퓨터 자원의 사용량이 상당히 증가하게 됩니다.
또한 new연산자는 라이브러리 개발자가 원하는 메모리 할당 방식으로 커스터마이징 할 수 없습니다.
allocator 클래스를 사용하면 위 단계들을 각각 원할 때 사용할 수가 있습니다.

대표적인 컨테이너인 vector를 보면 우리는 벡터를 사용할 때 타입 매개변수로 하나만을 전달합니다.
하지만 사실 vector는 기본 타입 매개변수로 아래와 같이 allocator가 하나 더 사용됩니다.

위처럼 특정 컨테이너에 최적화된 유연한 메모리 사용과 관리를 위해 대부분의 컨테이너들은 allocator를 사용합니다.

그럼 대체 allocator가 무엇이 좋은지 한번 살펴보겠습니다.

우선 메모리를 동적 할당할 경우 초기화되지 않은 공간(uninitialized)으로 메모리를 allocate 할 수 있습니다.
이게 무슨 말이냐면 new 연산자의 경우 메모리를 할당하고 기본적으로 값 또는 객체를 의무적으로 초기화 해줍니다.
이는 요소가 많을 수록 오버헤드가 높아지게 되고 초기화를 원치 않을 경우에도 불필요한 오버헤드가 발생하게 됩니다.

예를 들어 class 타입 요소를 갖는 크기가 10인 벡터를 선언했다고 가정해 봅시다.
만약 내부적으로 벡터가 new 연산자를 통해 생성된다면 벡터는 할당과 동시에 모든 요소들이 해당 클래스 타입에 맞게 기본 생성자를 통해 초기화가 되어 있을 것입니다. 그리곤 사용자가 이 벡터에 원하는 요소를 저장하겠죠. 이는 각 요소들을 두 번씩 초기화하는 셈입니다. 불필요한 초기화가 선택이 아닌 필수적이라는 것이죠. 쉽게 말해 int a = 3;를 int a = 0; a = 3; 두 단계를 거친다는 뜻입니다. 만약 이러한 작업을 수십만번 반복한다고 생각하시면 이해가 빠르리라 생각됩니다. 

만약 allocator의 멤버 함수를 이용하면 메모리의 할당은 되었지만 초기화 되지 않은 상태의 메모리의 시작 주소를 얻을 수 있습니다.원래 초기화되지 않은 메모리 공간에 객체를 직접 할당할 수 없습니다. 하지만 해당 클래스의 멤버 함수 또는 관련 함수가 초기화 되지 않은 공간에 객체를 저장할 수 있도록 해줍니다.

그리고 할당받은 메모리에 객체를 생성 후 메모리 해제(deallocate)없이 생성한 객체들을 소멸(destroy)시킬 수 있습니다. 즉 메모리의 재할당 없이 그 공간을 allocate 했던 초기 상태로 만들 수 있습니다. new 연산자로 할당한 메모리 공간은 delete를 사용하면 메모리 공간이 해제가 되죠.  즉 하나의 과정을 세밀하게 나누어 컨트롤할 수 있는겁니다.

또한 해당 라이브러리는 할당받은 메모리 공간 중 객체가 생성된 공간과 아직 초기화되지 않은 공간을 알 수 있는 방법을 제공합니다. 

 allocator 클래스는 주로 4개의 멤버 함수와 멤버 함수는 아니지만 용도에 따라 관련된 함수 2개를 사용합니다.
 

template <class T>
class allocator
{
public:
   T* allocate(size_t);
   void deallocate(T*, size_t);
   void construct(T*, const T&);
   void destory(T*);
   
   ....
};

template <class In, class For>
For uninitialized_copy(In, In, For);

template <class For, class T>
void uninitialized_fill(For, For, const T&);

 
allocate 함수는 초기화되지 않은 메모리 공간을 할당하여 그 시작 주소를 반환하는 함수입니다.

매개변수는 바이트 단위가 아닌 필요한 T 객체의 개수(n)이며  인자로 전달된 개수만큼 T타입의 객체를 충분히 할당할 수 있는 공간을 만듭니다. 충분히라는 말이 어떤 의미인지 디버거로 확인해보았는데 1바이트 타입이든 4바이트 타입이든 8바이트든 그 이상이든 할당된 크기 +  4바이트만큼 공간을 할당해주는 것 같습니다. 예를 들어 8바이트 타입의 객체를 5개 저장할 공간을 만들고 싶다면 인자로 5를 전달합니다. 그러면 8bytes * 5 + 4bytes = 44bytes가 할당되고 그 시작 주소를 반환합니다.



deallocate 함수는 메모리 공간을 해제하는 함수입니다.

인자로 포인터와 개수를 받으며 포인터는 allocate로 할당했던 메모리의 시작 주소를 가리키는 포인터이며 개수는 allocate로 전달했던 인자인 개수입니다. cpp 레퍼런스에는 반드시 allocate로 전달했던 인자의 개수와 동일해야 한다고 하는데 다르게 넣어도 메모리 상에서 지워지는 것을 봤을 땐 동일한 것 같습니다. 무슨 차이인지 잘 모르겠네요. 그래도 사용할 땐 안전하게 사용하는 것이 좋겠죠. 어쨋든 deallocate 함수는 allocate로 할당한 공간을 해제하는 함수입니다.


construct 함수는 초기화되지 않은 공간에 요소를 저장하는 함수입니다.

T타입 포인터와 객체를 레퍼런스로 받으며 포인터가 가리키는 위치에 객체를 저장합니다. 초기화되지 않은 공간에 *(간접참조) 연산자를 사용하여 값을 대입할 경우 에러가 발생합니다. 이 함수를 통해 에러없이 저장할 수 있습니다.



destroy 함수는 객체를 소멸시킵니다.

소멸과 메모리 해제는 다릅니다. T타입 포인터를 인자로 받으며 포인터가 가리키는 위치의 객체의 소멸자를 호출합니다. 즉  인자로 전달된 포인터가 가리키는 객체의 소멸자를 호출합니다. destory를 호출하지 않고 deallocate를 호출할 경우 각 요소에 저장된 객체는 사라지겠지만 사라진 객체가 가리키던 객체는 그대로 메모리에 남아있어 메모리 누수(memory leak)가 발생할 수 있습니다.


uninitialized_copy 함수는 멤버 함수는 아니지만 자주 사용되는 관련된 함수입니다.

STL의 std::copy함수와 비슷하며 입력 반복자 2개(first, last)와 순방향 반복자 1개(out)를 인자로 받습니다. [first, last) 범위의 요소들을 out이 가리키는 위치에 순서대로 복사합니다. 그리고 이 함수는 복사가 완료된 위치의 다음 요소를 가리키는 포인터를 반환합니다. 즉 1~5 위치에 복사를 완료했다면 6위치를 가리키는 포인터를 반환합니다.


uninitialized_fill 함수는 주어진 범위의 공간을 3번째 인자로 주어진 값으로 채웁니다.
ex) uninitialized_fill(first, last, val) // [first, last) 범위의 각 모든 요소에 val을 저장합니다.


함수의 사용 예시는 나중에 Function 카테고리에 올리도록 하겠습니다.




댓글