관리 메뉴

IT 컴퓨터공학 자료실

중급편 5. malloc () 된 포인터 사용 본문

컴퓨터공학/포인터

중급편 5. malloc () 된 포인터 사용

윤맨1 2015. 6. 30. 11:18

                                           포인터 토대가 된 책

                                        malloc () 된 포인터 사용 

그런데 메모리를 확보 할 유용한 라이브러리 함수 malloc (3)이있다. 이것 자체는 결코 OS의 기능으로 구현되는 시스템 콜은 아니지만 필요에 따라 메모리를 확보하는 시스템 호출 sbrk (2)를 호출한다. malloc (3)는 동적으로 메모리를 확보하기 위해 사용된다. 즉, 런타임에만 크기가 정해지지 않은 배열을 확보하는 데 사용된다. 예를 들어, 편집기 등을 생각해 보면 그 문서를 메모리에 저장하기 위해 메모리가 확보되는 것이지만, 그 크기는 문서의 크기에 의해서만 결정된다.여기에 정적 배열로 확보하면 특정 크기 이상의 파일은 인식 할 수없는 것이다. 이것은 곤란하다. 그래서 이런 경우에는 malloc (3)을 사용하여 동적으로 필요한만큼의 메모리를 확보하는 것이다.

malloc (3)은 void 형 포인터를 돌려 준다. 어떤 크기의 구조체에서도 malloc (3)에서 확보 할 수 있지만, malloc (3)이 반환 포인터는 void 형이므로 캐스트 필요가있다.

struct Point {
    int x;
    int y;
};   
/ * 여기서 아직 구조체의 구조를 선언했을뿐,
실체는 확보되어 있지 않다. * /

struct Point * p;  
/ * 여기서 아직 포인터 밖에없고, 실체는 확보
되어 있지 않다. * /
p-> x = 10;     
/ * 실체가 없기 때문에 세그먼트 폴트
등을 일으킬 * /
p = (struct Point *) malloc (sizeof (struct Point));
/ * 여기서 드디어 실체가 확보된다. * /
p-> y = 10;     
/ * 제대로 확보 된 실체에 값이 포함될 * /
free (p);
/ * 사용 끝나면 메모리는 반환한다.

어디서나 포인터를 선언하는 것은 가능하지만, 그 포인터에 대한 실체의 확보는 별개이다. 포인터의 선언적 확보에 메모리를 동반 한 실체를주는 것이 malloc (3)의 역할이다. free (3)은 malloc (3)에 의해 확보 된 메모리가 "이제 사용하지 않는다」상태에있을 때, 재사용을 위해 반환한다.

malloc (3)의 유일한 인수는 확보 된 메모리의 크기이다. 구조체 메모리를 확보 할 경우에는 보통 sizeof 연산자가 사용된다. sizeof 연산자는 구조체 선언에서 그 구조체의 크기를 자동으로 계산한다. 이것은 수동으로 계산하는 것보다 좋다. 왜냐하면 구조는 "패딩 문제"가 있고, 컴파일러가 최적화를 위해 함부로 구조 속에 '먹거리'를 넣어 버릴 가능성이 있기 때문이다.

는 것은 구조체의 배열도 확보 할 수있다. 배열 이름은 포인터와 다름 없기 때문에, 다음과 같은 확보 방법 좋다.

struct Point * points = 
    (struct Point *) malloc (sizeof (struct Point) * 10);
if (points == NULL) {
        printf ( "memory can not alloc! \ n");
        exit (1);
}
for (i = 0; i <10; i ++) {
        points [i] .x = i;
        points [i] .y = i;
}

malloc (3)은 실패 할 가능성이 높은 라이브러리 함수이다. 왜냐하면 메모리를 확보 할 수없는 경우에는 malloc (3) 조용히 NULL 포인터를 반환한다. 즉, malloc (3)을 사용하는 경우에는 반환 값의 체크 (NULL이 아닌 것)가 필수적이다.

이때 대부분의 독자는 "왜 접근이 points [i] -> x가 아닌 걸까?"라는 의문을 가진지도 모른다. 이것은 다음의 이유에 의한 다.

points 포인터이며, 확보 된 메모리의 선두를 가리키고있다. 이것을 배열 참조하면, 그 자체는 제대로 확보 된 구조로되어 있으며, 결코 points []는 포인터가 아니라 거기에서 메모리가 확보 된 실체이다. 그냥

int * ints = (int *) malloc (sizeof (int) * 10);
if (ints == NULL) {
        printf ( "memory can not alloc! \ n");
}
ints [5] = 10; 
/ * ints [5]는 포인터가 아니다 * /

이다 것과 마찬가지이다. 실수하기 쉽기 때문에주의하라.

malloc (3) 시스템 호출이 아니라 라이브러리 함수이다. 실제로 시스템 호출 인 sbrk (2) 내부에서 호출하여 메모리 관리를 실시하고있다. 이 malloc (3)의 구현은 K & R에 설명이 있으므로 참조하면 좋을 것이다.

malloc (3)의 구현을 보면 뜻밖의 일로 알게된다. 그것은 free (3)의 구현 부분이다. 즉, free (3) 무엇을하고 있는지라고하면 불필요하게 메모리를 재사용을 위해 "가능한 메모리 목록"에 덧붙여있는 것이다. 그래서 free (3)는 메모리를 해제하지 않는다. UNIX 등의 가상 메모리 환경에서 이것은 치명적인 문제가 아니다. 즉, 사용되지 않는 메모리 (페이지)는 한동안 메인 메모리에서 쫓겨나 버리고 현재 사용중인 능률적 인 데이터가 들어있는 페이지가 메인 메모리에 두는 것이다. 이 논리는 가상 메모리 환경에서만 문제가있는 경우가 많다. 일단 MS-DOS의 시대에는 메모리 제한이 엄격하므로, malloc (3), free (3)에 의한 메모리 관리를 회피하여 MS-DOS 시스템 호출에서 메모리를 확보하고 관리하는 것이 보통이었다 . 즉, 불필요하게 메모리를 수동으로 해제하지 것이다. 이 지식이 도움이 될 수도없고도 없을 테니 메모 해 둔다.

또한 free (3)는 리턴 값이없는 ( void free (void * ptr); 로 선언되어있다). 하지만 malloc (3)는 라이브러리 함수이다. 즉, 이것은 다음을 의미한다.

  1. malloc (3), free (3)는 사용자 메모리 공간에서 실행된다.
  2. 즉, malloc (3)이 이용하는 글로벌 관리 데이터는 사용자 메모리 공간에 존재한다.
  3. 그것은 어디에 있는가하면, 소스 레벨에서는 라이브러리에 있으며, 이는 실제 프로그램에 연결하면 프로그램의 데이터 세그먼트에 정리한다.
  4. 것은 malloc (3)이 이용하는 글로벌 관리 데이터는 사용자 프로그램에 의해 손상 될 가능성이있다.

알 일까? 결과적으로 malloc (3)이 이용하는 글로벌 관리 데이터 (그들은 확보 된 메모리에 목록의 형태로 참조를 가지고있다)가 파괴 된 때에는, free (3)을 호출 할 때 세그멘테이션 폴트 이 일어날 것이다. 이 free (3)에서 세그멘테이션 폴트가 일어나는 버그는 그 원인을 파악하는 것이 상당히 어려운 버그이다. 디버거에 의해서가, 원인을 모르는 경우가 대부분이며,주의 깊게 소스를 검토하거나 Electric Fence와 같은 디버깅 malloc (3) 라이브러리를 사용하여 원인을 규명 할 필요가있다. Electric Fence 라이브러리는 malloc (3)과 free (3)을 대체 라이브러리이며, 보통의 malloc (3), free (3)보다 엄격한 라이브러리 사용을한다. 즉, 연속 메모리를 확보하지 않고 날아 나는 메모리를 확보하여 확보 한 용량을 초과 액세스가 있던 시점에서 확실하게 세그멘테이션 폴트가 생성되도록한다. 그래서 이때 디버거를 사용 주면 그 원인이되는 액세스를 특정 할 것이다. 유효한 라이브러리이기 때문에, 사용해 보면 좋을 것이다.

이 malloc (3)에 의한 메모리 확보는보다 정교한 방법이있다. 그것은 realloc (3)을 사용 방식이다. 이것은 일단 확보 된 메모리의 크기를 늘리거나 구부리지 위해 사용한다. 예를 들어, 편집기 인 파일을 읽어 들여 처리가 끝난 후 다른 크기 다른 파일을로드하면, 문서를 저장하고 있던 메모리를 재사용하고 싶어진다. 이때 realloc (3)은 확보 된 메모리의 크기를 조정 해 준다. 이것은 매우 중요한 방법이며, 중 · 상급에서는 메모리 확보에 대한 거의 정형적인 방법이된다.

realloc (3)은 2 개의 인수를 취한다. 첫 번째는 현재의 크기가 변경되어야 포인터이며, 제 2의 인수는 변경 후의 크기이다. 첫 번째 인수는 NULL이라도 상관 없다. 그때는 malloc (3)과 같은 동작을한다.

int Points_Alloced = 0;
struct Point * alloc_mem (struct Point * buff, int size) {
        / * 만약 크기가 다르면 * /
        if (size! = Points_Alloced) {  
                void * tmp = realloc (buff, size);
                / * 오류 체크 * /
                if (tmp == NULL) {    
                        fprintf (stderr, "can not realloc memory");
                        / * 확보하지 못하면 원래 반환
                           것이 옳다 경우도 많다 * /
            exit (1);
                }
                / * 확보 크기를 갱신 * /
                Points_Alloced = size;   
                return (struct Point *) tmp;
        } else {
       / * 크기가 같다면 원래대로 * /
                return buff;  
        }
}

void main () 
{
        struct Point * Points;
        Points = alloc_mem (NULL, 10);
        ..............
        Points = alloc_mem (Points 20);
        ..............
        Points = alloc_mem (Points 5);
}

이 예에서는 크기가 작아 할 때도 실제 배열 크기를 축소하고 있지만, 일반적으로는 확대 할 때만 변경하면 좋겠다. 이 realloc (3)에서 확보 된 메모리를 사용할 때 하나주의가 필요하다.

void main ()
{
    struct Point * Points = NULL;
    struct Point buff;
    int num = 0;
    int alloced = 0;
    while (fread (buff, sizeof (buff) 1, stdin)) {
        if (+ num> alloced) {
            Points = alloc_mem (Points, alloced +10);
            alloced + = 10;
        }
        Points [num] .x = buff.x;
        Points [num] .y = buff.y;
    }
}

이것은 안전이며, 10 개 단위마다 메모리를 확보하기 위해 효율도 좋다. 이것이 왜 안전한가? 생각하면 Points에 배열 액세스 할 수 있기 때문이다. 즉, 다음의 프로그램은 안전하지 않다.

void main ()
{
    struct Point * Points;
    struct Point * at;
    struct Point buff;
    int num = 0;
    int alloced = 0;

    Points = alloc_mem (NULL, 10);
    alloced = 10;
    at = Points;
    while (fread (buff, sizeof (buff) 1, stdin)) {
        if (+ num> alloced) {
            Points = alloc_mem (Points, alloced +10);
            alloced + = 10;
        }
        * at-> x = buff.x;
        * at-> y = buff.y;
        at ++;
    }
}

이것은 알기 쉬운 예로 들고있다. at 먼저 확보 된 Points를 나타내고 있지만, 배열이 확대되면 Points 내용 (확보 된 메모리)는 이동 될 수있다. 포인터 인 at은 그 동작과는 전혀 연동하지 않는다. 그래서 realloc (3)에 의해 무효가 된 포인터를 at 유지하고 계속 수있다. 이것은 눈에 좋지 않다고 느낄 것이다. 그러나이를 찾기 힘든 표현에 고쳐 쓸 수도있다.

int Alloced = 0;
int NowUsed = 0;
struct Points * Points = NULL;

struct Points * newPoint (void)
{
    if (NowUsed + 1> Alloced) {
        Points = alloc_mem (Points 10);
        Alloced + = 10;
    }
    return & Points [NowUsed ++;
}


void main ()
{
    struct Point * at * prev;
    struct Point buff;

    prev = NULL;
    while (fread (buff, sizeof (buff) 1, stdin)) {
        at = newPoint ();
        if (prev! = NULL) {
            at-> x = buff.x + prev-> x;
            at-> y = buff.y + prev-> y;
        } else {
            at-> x = buff.x;
            at-> y = buff.y;
        }
        prev = at;
    }
}

이때 prev은 그것이 집합 된 단계에서는 유효했지만, realloc (3)에 의해 비활성화 된 포인터를 보유 할 가능성이있다. 이것은 찾기 힘든 버그를 일으킨다. 그래서 이것은 다음과 같이 재 작성하여야한다.

int Alloced = 0;
int NowUsed = 0;
struct Points * Points = NULL;

int newPoint (void)
{
    if (NowUsed + 1> Alloced) {
        Points = alloc_mem (Points 10);
        Alloced + = 10;
    }
    return NowUsed ++ ;
}


void main ()
{
    int at, prev ;
    struct Point buff;

    prev = -1 ;
    while (fread (buff, sizeof (buff) 1, stdin)) {
        at = newPoint ();
        if (prev! = -1 ) {
             Points [at] .x = buff.x + Points [prev] .x ;
             Points [at] .y = buff.y + Points [prev] .y ;
        } else {
            Points [at] .x = buff.x;
             Points [at] .y = buff.y;
        }
        prev = at;
    }
}

이것은 약간 아이러니이지만, 포인터 약간 고급 관용구이다 realloc (3)를 사용하기 위해서는 배열로 접근하지 않으면 안된다. 그 이유는 지금까지 본 적이 같다.포인터에 액세스하면이 때 포인터는 "배열베이스 + 인덱스 '역할을하고이를 저장한다. 배열의 기반이 하청 함수 내에서 변경되었을 때, 포인터 자체가 업데이트되는 것은 아니다 것이 버그를 일으킨다. 만약 배열에 액세스하려고한다면, 컴파일러는 이렇게 생각한다.

  1. 하청 함수를 호출하고있다.
  2. 전역 변수 Points []는 안전하지 않다.
  3. 따라서 참조 Points [at]은 다시 계산 ( p = Points + at * sizeof (Points) ) 필요가있다.

라고하는 흐름에 의해 위험한 최적화는 억제된다. 그러나 명시 적하지 않는 형태로 Points [] 재 할당이있을 수 경우도 없지 않다. 전형적인 케이스는 스레드를 사용하는 경우이다. 이러한 경우에는 처리에 대해 배타 제어 (MUTEX)을주의 깊게 붙이는 등의 배려가 필요하다.

이 realloc (3)의 기술은 응용 프로그램에서 마음대로 정한 하드 코딩 된 "최대 ** 수"제한을 넘어서는 메모리가 사용 가능한 범위에서 가장 큰 "** 수"를 실현하는 . 이것은 매우 효과적인 기술이며, 당신의 프로그램에서도 꼭 채택해야 기술이다. 또한보다 고급 메모리 관리 기술이며, 가비지 컬렉터 (GC)도이 realloc (3)의 기술 위에 구축되는 것이 상식이다.

과제

  1. 목록 구조를 realloc (3)을 사용하여 동적 배열 확보로 가보자. 이것은 중상급 프로그램은 거의 일상적인 작업이며, 실제 프로그램에서는 만족스런 경험한다.