반응형
728x90
반응형

C++은 다른 언어들처럼 가비지 컬렉터가 없다ㅋㅋ 그래서 메모리 관리를 제대로 하지 않으면, 정확히 Heap 영역에 대한 메모리 관리가 제대로 되지 않는다면,

헬이다. 헬.

그러면 어찌해야하는가? RAII 라는 디자인 패턴이 있다.

Resource Acquisition Is Initialization 

위를 줄여서 RAII 라고 부른다.

RAII의 유래

그럼 왜 RAII일까? 유래를 좀 찾아봤는데 자원의 획득은 초기화이고, 반대로 자원의 반환은 객체 소멸 뭐 이런 식으로 확장하여 나타내는 어떤 관용구 같은 느낌의 디자인 패턴 개념이다.

RAII란?

좀 더 자세하게 관련해서 정의를 알아 보자면 RAII 는 객체의 생명주기에 관련된 내용으로, C++에서 객체가 생성되고 사용된 Scope를 벗어나면 객체의 자원을 해제해주는 방법을 이야기한다.

즉, 힙에 동적 할당되는 객체라고 해도, 프로그램 실행 흐름이 해당 객체의 유효 scope를 벗어나면 객체를 메모리 해체하도록 하는 것이 RAII라고 생각하면 될 것 같다.

근데 아쉽게도 힙 영역에 할당된 객체는 지역 scope를 벗어난다고해서 동적할당된 부분이 해체되지 않는다. 왜냐하면 동적할당이 이루어지는 해당 scope 내에는 객체 포인터가 스택에 쌓이고, 해당 scope가 끝나면 그 객체 포인터만 스택에서 소멸된다. 동적할당된 메모리는 힙 영역에 그대로 남아있다.

다음과 같이 말이다.

번호순으로 읽기

위의 사실을 응용하여,

어떤 scope에서 동적 할당이 이루어졌다면, 해당 scope에서 스택 영역에 할당되는 요소에게 동적 할당한 주소를 넘겨주고, 그 스택 영역에 할당된 요소는 스택에서 제거될 때, 넘겨받은 동적 할당된 주소를 함께 delete 하면서 제거되면 되지 않을까? 라는 발상이 떠오른다. 아래와 같이 말이다.

번호순으로 읽기

문제가 되는 부분

자, 일단은 문제가 되는 코드이다. 위의 그림과는 살짝 다르지만 문제 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
 
class R
{
    private:
        int *data;
    public:
        R(int len)
        {
            this->data = new int[len];
            std::printf("R() call! : new int[%d]\n",len);
        }
        ~R()
        {
            delete []this->data;
            std::printf("~R() call! : delete [] \n");
        }
};
 
void function(void)
{
    R * r = new R(1000);
 
    std::printf("function() 함수 종료\n");
    return;
}
 
int main(void)
{
    function();
    return 0;
}
cs

위의 코드에서 function 함수라는 지역 scope 내에서 객체 R을 동적할당하고, function 함수를 그냥 종료하는 모습이다. 동적 할당된 R 은 소멸되지 않고 그대로 남다가 해당 프로세스가 종료될 때, 비로소 운영체제에 의해서 메모리 자원이 회수된다. 이런 회수를 원한게 아닌데 말이다.

그렇다면 실행 결과를 보자.

분명히, 소멸자가 호출되지 않은 모습이다. 이것이 바로 메모리 누수(Leak)

스마트 포인터

이를 해결할 방법으로 스마트 포인터라는 것이 등장한다. 포인터 역할을 하는 객체로 이 스마트 포인터를 스택 객체로 할당하고 동적 할당 주소를 넘겨주면 스마트 포인터 객체가 소멸될 때 동적 할당된 힙까지 알아서 똑똑하게 삭제를 해주기 때문이다.

이러한 스마트 포인터를 직접 구현해보았다. 다른 이미 구현된 스마트 포인터만큼 스마트하지는 못해서 그냥 오토 포인터라고 이름을 지었다ㅋㅋㅋ

뭐 아래의 연산자 오버로딩 포스팅 때, 구현을 이미 해봤지만 그래도 새롭게 참고하면서 구현해보았다.

 

21 연산자 오버로딩

연산자 오버로딩 연산자 오버로딩 C++언어에서는 연산자의 오버로딩을 통해서 기존에 존재하던 연산자의 기본 기능 이외에 다른 기능을 추가할 수 있다. 먼저 15번 라인을 통해서 operator+ 라는 함

typingdog.tistory.com

코드 및 실행결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
 
class R
{
private:
    int* data;
    int len;
public:
    R(int _len)
    {
        this->len = _len;
        this->data = new int[_len];
        for (int i = 0; i < _len; i++)
            this->data[i] = i * 100000;
        std::printf("R() call! : new int[%d]\n", _len);
    }
    ~R()
    {
        delete[]this->data;
        std::printf("~R() call! : delete [] \n");
    }
    void ShowData(voidconst
    {
        for (int i = 0; i < this->len; i++)
            std::printf("%d, "this->data[i]);
        std::cout << std::endl;
        return;
    }
};
 
template <typename T>
class AutoPointer
{
private:
     const T* ptr;
public:
    AutoPointer(T* _ptr) : ptr(_ptr)
    {
    }
    ~AutoPointer()
    {
        delete this->ptr;
    }
    const T& operator* () const
    {
        return *(this->ptr);
    }
    const T* operator-> () const
    {
        return this->ptr;
    }
};
 
void function(void)
{
    AutoPointer<R> r(new R(10));
    r->ShowData();
    std::printf("function() 함수 종료!\n");
    return;
}
 
int main(void)
{
    function();
    return 0;
}
cs
힙 영역의 객체의 소멸까지 책임지는 스마트 포인터 똑똑해^^7

이렇게 힘들게 만들었는데 이미 C++ 11에서는 다음과 같은 문법을 제공한다.

 

unique_ptr<> : #include <memory>

이미 11 문법에서는 겁나 스마트한 포인터 객체로 제공을 해주었던 것이다... 사용 방법은 내가 만든 오토 포인터와 같으니 바로 예제로 들어가보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <memory>
 
class R
{
    private:
        int *data;
    public:
        R(int len)
        {
            this->data = new int[len];
            std::printf("R() call! : new int[%d]\n",len);
        }
        ~R()
        {
            delete []this->data;
            std::printf("~R() call! : delete [] \n");
        }
};
 
void function(void)
{
    std::unique_ptr<R> r(new R(1000));//R * r = new R(1000);
    std::printf("function() 함수 종료\n");
    return;
}
 
int main(void)
{
    function();
    return 0;
}
cs

불안정한 메모리 관리를 이러한 패턴으로 처리한다는게 정말 잔머리를? 기가막히게 잘 굴린듯한 느낌이다. 그래도 GC(가비지 컬렉터)가 제공되면 참 좋겠지만 이렇게라도 일부라도 관리를 틈틈이 하는게 좋은 것 같다.

다음 번 포스팅에서는 (공부하다가 필요한 부분부분 정리하는 포스팅이기 때문에 시리즈물처럼 바로 다음 번은 아니지만) 이 unique_ptr 외에 공유 가능한, 그리고 인자로의 전달 시 어떤 형태로 코드를 짜야하는 지 등에 대해서 올려볼 것이다.

728x90
반응형
728x90
반응형

동적 메모리 할당

  • 메모리의 구성

프로그램 실행 시 운영체제에 의해서 마련되는 메모리 구조는 위와 같이 네 개의 영역으로 구분된다. 여러 영역으로 나뉘는 이유는 각기 다른 성향을 가진 여러 공간들을 유사한 성향의 공간끼리 분류해서 해당 성향을 지닌 데이터들을 분류한 공간 별로 저장하면 관리가 용이해지고 메모리의 접근속도가 향상된다.

  • 메모리의 동적 할당

함수가 매번 호출될 때마다 새롭게 할당되고 또 함수를 빠져나가도 유지가 되는 유형의 변수의 생성은 생성과 소멸의 시기가 지역 변수나 전역 변수와는 다른 유형의 변수를 생성하는 것인데 이를 가능케하는 개념이 메모리의 동적 할당이다.

malloc 함수와 free 함수

힙 영역은 프로그래머가 관리하는 메모리 공간이다. 필요한 메모리 공간을 프로그래머에 의해서 동적 할당하기 때문이다. 그렇기 때문에 메모리 할당 및 해체에 대해서 전적으로 프로그래머가 담당하기 때문에 malloc 함수로 할당한 메모리 공간은 free 함수로 꼭 해체해줘야 하며, malloc 함수에 의해 할당된 메모리 공간은 쓰레기 값으로 구성된다. 다음은 예제이다.

9번 라인을 통해 포인터 변수를 선언하고, 11번 라인을 통해 힙 영역 메모리 공간을 동적 할당하는데 이 때 인자로는 바이트 단위의 사이즈가 계산되어 전달되었고, 형 변환이 일어나고 있다. 원래 malloc 함수는 void * 형을 반환하도록 되어 있기 때문에 대입 연산을 위해서는 void * 형에서 원하는 형으로 변환을 거친 후에 참조를 진행해야 한다. malloc 함수가 void * 형을 반환하는 이유는? malloc 함수를 호출할 때 전달되는 것은 바이트 단위의 크기 정보 밖에 전달되지 않기 때문이다. 그렇기 때문에 malloc 함수는 어떤 자료형으로 참조할지 알 수 없는 상황이기 때문에 직접 형 변환 연산을 통해 변환을 진행하는 것이 그 이유이다. 그리고 18, 19번 라인을 통해 동적 할당한 메모리 영역에 값을 대입하고, 출력한다. 그리고 free 함수를 통해 동적 할당 해체를 진행한다.

  • free 함수의 호출이 필요한 이유

힙 메모리 영역은 프로그래머가 관리하는 영역이기 때문에 메모리의 동적 할당만큼 해체 또한 매우 중요하다. 할당한 만큼 해체해주지 않으면 운영체제에 의해서 메모리가 자동 해체되지만 그럼에도 free 함수를 통한 동적 할당 공간 해체가 필요한 이유는 더 큰 규모의 프로그램에서는 실행되는 동안 메모리 부족 등의 문제가 일어날 수 있기 때문에 습관적인 해체는 꼭 필요하다.

 

  • calloc 함수

malloc 함수와 똑같은 기능을 제공하나, 함수의 인자 전달 방식에 차이와 초기화 값의 차이가 있을 뿐이다. elt_size 크기의 블록 elt_count개를 힙 영역에 할당하는데, 해당 메모리 공간을 0으로 초기화 하라 라는 내용의 함수이다.

 

  • realloc 함수

realloc 함수는 ptr이 가리키는 메모리의 크기를 size 크기로 다시 조절하는 함수이다.

반환 값에 따라 두 가지 경우로 나눌 수 있다.

1.     malloc 함수가 반환한 주소와 realloc 함수가 반환한 주소가 같은 경우

-> 확장할 영역이 넉넉해서 기존 영역을 기반으로 공간을 확장한 경우

2.     malloc 함수가 반환한 주소와 realloc 함수가 반환한 주소가 다른 경우

-> 확장할 영역이 넉넉하지 못해서 새로운 영역에 별도로 할당한 경우

728x90
반응형

'컴퓨터 언어 정리 > C 언어' 카테고리의 다른 글

20 파일의 분할  (0) 2020.10.17
19 매크로 선행 처리기  (0) 2020.10.17
17 파일 입출력  (0) 2020.10.13
16 구조체  (0) 2020.10.12
15 문자와 문자열  (0) 2020.09.29

+ Recent posts