반응형

realloc

동적으로 할당된 메모리를 재할당합니다.

Declaration

void *realloc( void *memblock, size_t size )

Return value

성공 - 새로운 메모리 영역을 가리키는 포인터 반환
실패 - NULL 포인터 반환


Parameters

memblock - NULL 포인터 또는 이전에 할당한 메모리 영역을 가리키는 포인터
size     - 새로 할당받을 메모리 크기(단위는 바이트)


Detail descriptions

realloc()를 번역할 때 "재할당" 또는 "다시 할당"이라고 하는데 많이 어색합니다. 다시 할당하는 것이 아니라 할당된 메모리 영역의 크기를 변경, 대부분은 확장하는 것입니다. 정확하게 이름을 짓는다면 extendalloc()정도가 좋겠습니다. 굳이 "재할당(realloc)"이라고 이름 지은 것은 대부분 새로운 메모리를 할당받기 때문입니다.

 

realloc()에 전달되는 메모리 영역의 크기는 이전에 할당됐던 영역보다 클 수도 있고 작을 수도 있습니다. 대부분 메모리가 부족했기 때문에 다시 할당 받는 것이지만, 작은 경우가 있을 수도 있습니다. 그러나, 작은 경우라면 메모리를 새로 할당할 이유가 없습니다. 메모리가 부족한 경우에 인접해 있는 연속된 구간을 확보할 수 있다면, 다시 말해 현 상태에서 늘릴 수 있는 영역이 있다면 새로 할당하지 않아도 됩니다. 그러나, 대부분은 그렇지가 못하므로 기존 메모리를 해제하고 다른 위치에 할당하게 됩니다. [C 언어 표준에서는 어떻게 할당해야 한다는 것에 대해서는 언급하지 않습니다. 이 모든 상황은 구현에 따라 달라질 수 있습니다.]

메모리를 새로운 위치에 할당하려면 아래 순서에 맞게 작업해야 합니다.

  1. 새로운 위치에 지정한 크기만큼 메모리를 할당합니다.
  2. 이전 영역에 있던 내용을 새로운 영역에 복사합니다.
  3. 이전 영역의 메모리를 해제합니다.

realloc()는 이와 같이 동작합니다. 새로운 영역을 할당받더라도 현재 내용을 잃어버리는 일은 절대 없습니다. realloc()를 흉내낸 코드가 "Example codes" 항목에 있습니다. 참고하기 바랍니다.

그러나, 이와 같은 방식에는 엄청난 결함이 있습니다. realloc()는 새로운 위치에 할당될 수 있기 때문에, 이전 영역을 가리키는 모든 포인터 변수를 새로운 주소로 변경해야 합니다. 그렇게 할 수 없다면 해제된 영역을 가리키는 포인터가 언젠가는 문제를 일으킬 것입니다. 동적으로 할당한 메모리가 있고 여러 개의 포인터가 공유하고 있다면, 절대 realloc()를 사용해서는 안됩니다. 악명높은 덜 떨어진 포인터, 댕글링(dangling) 포인터가 탄생합니다.

realloc()는 어떤 경우를 실패라고 생각할까요? malloc()라면 할당할 메모리가 부족한 경우에 NULL 포인터를 반환합니다. 첫 번째는 확장에 실패한 경우입니다. realloc()의 목적은 메모리를 확장해서 옮기는 것인데, 현재 위치에서뿐만 아니라 새로운 위치로도 옮겨가지 못하면 NULL 포인터를 반환합니다. 이때 이전 영역에 있던 내용은 그대로 남아 있습니다. 두 번째는 새롭게 할당할 크기를 0으로 지정한 경우입니다. 크기가 0이라는 것은 새로운 할당으로 간주하지 않고, 이전에 사용하던 영역의 해제로 봅니다. free() 함수를 호출하는 셈입니다. 사용하던 영역은 해제되어 다음 번 동적 할당에서 사용할 수 있게 되고, NULL 포인터를 반환합니다. NULL 포인터의 반환이 너무 당연한 것이 할당받은 메모리가 없으므로 가리킬 곳도 없습니다. 그러나, 메모리를 해제하기 위해 realloc()를 사용해서는 안됩니다. free()로 해제할 때 가장 분명한 코드가 나오기 때문에 가독성에서 너무 좋지 않은 행동입니다.

memblock 매개 변수에 NULL 포인터를 전달하는 것은 malloc()를 호출하는 것과 같습니다. 사용중인 메모리가 없기 때문에 해제할 메모리도 없고, 결국 할당만 일어나게 되므로 malloc() 호출과 같습니다.

realloc()가 성공할 때의 반환값이 두 가지가 있습니다. 첫 번째는 현재 위치에서 확장에 성공한 경우로, memblock 매개 변수에 전달된 주소와 같습니다. 두 번째는 연속된 메모리가 없어서 새로운 위치에 할당된 경우로 memblock 매개 변수와 다른 주소입니다. 주의해야 할 것이 있다면 현재 위치에서 확장될 수 있다고 생각하는 것입니다. 함수 이름이 주는 모호함 때문에 자칫 엄청난 실수를 할 수 있습니다.

간혹 realloc()가 필요한지 의아할 때가 있습니다. 메모리가 부족한 상황을 만나기 전에 리스트(list) 자료구조를 사용하거나 엄청난 크기로 할당한 다음 나누어 쓰는 방법이 있습니다. 동적으로 할당한 메모리를 여러 개의 포인터 변수가 가리키는 상황이 생길 수 있다면, 지금 얘기한 방법들을 진지하게 고민해야 할 것입니다.


Remarks

메모리 동적 할당 함수인 calloc(), malloc(), realloc()를 비교한 표가 malloc()에 있습니다. 참고하기 바랍니다.


Header files

<stdlib.h>   <malloc.h>


Example codes

  1. realloc()로 메모리를 "확장"하거나 "축소"할 때 발생하는 포인터 변화를 보여줍니다.
  2. 먼저 NULL 포인터를 전달해서 메모리를 할당한 다음, 사용중인 크기보다 두 배 크게 "확장"하고, 최초 크기보다 두 배 작게 "축소"해서 결과를 비교합니다.
  3. 출력 결과를 보면 "확장"할 때는 반환된 주소가 사용중인 주소와 다르지만, "축소"할 때는 사용중인 주소와 같습니다. "축소"할 때는 사용중인 메모리를 재활용하고 있음을 알 수 있습니다.
  4. 그러나, C 언어 표준에서는 재활용할 것을 요구하지 않습니다. 컴파일러를 구현하는 회사측에서 어떻게 구현할지 결정할 따름입니다.
  5. free()는 한번만 사용했습니다. realloc()를 세 번 호출했지만, 두 번째 호출부터는 내부적으로 free()를 호출하기 때문에 realloc() 호출 횟수만큼 free()를 호출해서는 안됩니다.

#include <stdio.h>
#include <stdlib.h>

void main()
{
    int* array = NULL;
    const int size = 5;

    array = realloc( NULL, size*sizeof(int) );

    printf( "[확장]\n" );
    printf( "old - %d\n", array );
    array = realloc( array, size*sizeof(int)*2 );
    printf( "cur - %d\n", array );

    printf( "\n[축소]\n" );
    printf( "old - %d\n", array );
    array = realloc( array, size*sizeof(int)/2 );
    printf( "cur - %d\n", array );

    free( array );
}

[출력 결과]
[확장]
old - 4397872
cur - 4397776

[축소]
old - 4397776
cur - 4397776


  1. realloc()의 기본 사용법을 보여줍니다.
  2. NULL 포인터를 전달해서 malloc()처럼 사용하는 방법과 사용중인 메모리를 두 배로 확장했을 때의 결과를 보여줍니다.
  3. malloc()처럼 바이트 단위로 동작하기 때문에 요소 개수와 요소 크기를 곱해서 전달합니다.
  4. 출력 결과를 보면, 사용중인 메모리를 20바이트에서 40바이트로 확장하면 사용중인 영역이 확장되는 것이 아니라 새로운 영역에 내용을 복사합니다. 사용중인 영역의 내용은 손상없이 복사되지만, 확장된 영역은 초기화되지 않고 쓰레기로 채워집니다.
  5. 동적 배열을 하나만 사용하고 크기도 20바이트를 확장했을 뿐인데, 새로운 주소를 반환한다는 것은 사용중인 영역의 연속된 구간을 할당하는 것이 얼마나 어려운지 보여줍니다. 출력 결과에서 []로 감싼 숫자가 할당받은 주소인데 서로 다릅니다. 절대 사용중인 주소를 계속해서 사용할 수 있을 거라고 기대해선 안됩니다.
  6. MultiArray()는 배열을 초기화하는 함수로, 0부터 시작해서 multi의 배수로 채웁니다.

#include <stdio.h>
#include <stdlib.h>

void MultiArray( int* array, int size, int multi );
void ShowArray( int* array, int size );

void main()
{
    int* array = NULL;
    int  size = 5;

    array = realloc( NULL, size*sizeof(int) );

    MultiArray( array, size, 3 );
    ShowArray( array, size );

    size *= 2;
    array = realloc( array, size*sizeof(int) );

    ShowArray( array, size );

    free( array );
}

void MultiArray( int* array, int size, int multi )
{
    int i;
    for( i = 0; i < size; i++ )
        array[i] = i*multi;
}

void ShowArray( int* array, int size )
{
    int i;
    printf( "[%d]", array );

    for( i = 0; i < size; i++ )
        printf( " %d", array[i] );

    printf( "\n" );
}

[출력 결과]
[4397776] 0 3 6 9 12
[4397680] 0 3 6 9 12 -842150451 -842150451 -842150451 -842150451 -842150451


  1. 사용중인 메모리의 크기를 늘려주는 realloc()를 만들어 봅니다. ExtendArray() 내부에서 calloc()로 새로 만든 메모리를 초기화합니다. 프로그램은 0보다 작거나 같은 정수를 입력하면 종료합니다.
  2. realloc()와 똑같이 만드는 것은 불가능합니다. 간단하게 realloc()의 특징일 수 있는 것만을 일부 구현합니다.
  3. realloc()를 호출하는 상황이 네 가지 있습니다. 이들 네 가지는 "최초 할당"과 "축소", "확장", "변화 없음"으로 부를 수 있습니다.
  4. "최초 할당"은 사용중인 메모리 주소 대신 NULL 포인터가 전달되는 경우로, 사용중인 메모리가 없다는 뜻이므로 별도의 코드없이 calloc()의 호출 결과를 반환합니다.
  5. "축소"와 "변화 없음"은 메모리 크기를 줄이는 것인데, 굳이 사용중인 메모리를 해제할 필요없이 기존에 사용하던 메모리를 재활용합니다. 크기를 줄이는 과정에서 남는 메모리를 해제할 수 있는 방법이 없으므로, 사용중이던 메모리를 해제할 때까지 그대로 둡니다. 일종의 낭비라고 생각할 수도 있겠습니다. ExtendArray() 내부적으로 메모리에 변화가 없으므로 매개 변수로 전달된 oldblock을 그대로 반환합니다.
  6. "확장"일 때 realloc()를 가장 많이 사용합니다. 연속된 메모리가 있다고 보장할 수 없기 때문에, 요청한 메모리를 새롭게 할당합니다. 사용중인 메모리의 내용을 새로운 메모리로 옮기고, 사용중인 메모리를 해제합니다.
  7. calloc()를 사용했으므로 "확장"된 메모리는 모두 0으로 초기화됩니다. 마지막 출력 결과를 보면, 요소가 3개에서 6개로 확장되었고, 확장된 요소에는 0이 들어갑니다. calloc()를 사용해서 할당했다는 증거입니다. 실제의 realloc()는 "확장" 메모리를 초기화하지 않는데, malloc()을 사용하는 것 같습니다.
  8. 출력 결과를 분석하기가 조금 어렵습니다. 첫 번째 결과는 "최초 할당"이고, 두 번째 결과는 "확장"이고 FillArray()를 호출했기 때문에 새로운 내용으로 채워졌습니다. 세 번째와 네 번째 결과는 "축소"이고, "축소"는 사용중이던 메모리를 해제하지 않기 때문에 이전 결과와 똑같이 나옵니다.
  9. FillArray()는 메모리를 확장할 때만 호출합니다. "축소"에서 사용중인 메모리를 유지한다는 것을 보여주기 위해 FillArray()는 "확장"일 때만 호출합니다. 그러나, "확장"에 대한 결과도 보여줘야 하므로 반복문을 벗어나면 무조건 2배로 확장합니다. 확장 메모리가 0으로 초기화된다는 것을 보여주기 위해, 이번에는 FillArray()를 호출하지 않습니다.
  10. 의도적으로 calloc()를 사용했지만, 메모리를 초기화하지 않는 malloc()를 사용해도 괜찮습니다.
  11. FillArray()와 ShowArray()는 배열을 난수로 채우고 화면에 출력하는 함수들입니다. 대단할 것이 없는 함수이므로 설명은 생략합니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void* ExtendArray( void* p, int cur, int want, int datasize );
void FillArray( int* array, int size );
void ShowArray( int* array, int size );

void main()
{
    int* array = NULL;
    int  want, use = 0;

    while( 1 )
    {
        printf( "개수 : " );
        scanf( "%d", &want );

        if( want <= 0 )
            break;

        array = ExtendArray( array, use, want, sizeof(int) );

        if( want > use )
            FillArray( array, want );

        use = want;
        ShowArray( array, use );
    }

    printf( "[calloc() 확인]\n" );

    want = use * 2;
    array = ExtendArray( array, use, want, sizeof(int) );
    ShowArray( array, want );
}

void* ExtendArray( void* oldblock, int count, int want, int datasize )
{
    void* block;

    if( oldblock == NULL )                 // 최초 할당
        return calloc( want, datasize );

    if( count >= want )                    // 축소, 변화 없음
        block = oldblock;
    else                                   // 확장
    {
        block = calloc( want, datasize );
        memcpy( block, oldblock, count*datasize );

        free( oldblock );
    }

    return block;
}

void FillArray( int* array, int size )
{
    int i;
    for( i = 0; i < size; i++ )
        array[i] = rand()%100;
}

void ShowArray( int* array, int size )
{
    int i;
    for( i = 0; i < size; i++ )
        printf( "%d ", array[i] );

    printf( "\n" );
}


[출력 결과]
개수 : 4
41 67 34 0
개수 : 9
69 24 78 58 62 64 5 45 81
개수 : 7
69 24 78 58 62 64 5
개수 : 3
69 24 78
개수 : 0
[calloc() 확인]
69 24 78 0 0 0

+ Recent posts