C 4주차 - 동적할당

Gunter·2024년 3월 24일
0

C

목록 보기
13/13
post-thumbnail

C언어의 메모리 구조

(https://zangzangs.tistory.com/107)

프로그램 실행 시 운영체제에 의해서 마련되는 메모리의 구조는 다음과 같이 네 개의 영역으로 구분이 된다.

  • 코드 영역 ( Code Area )
    코드 영역은 이름 그대로 실행할 프로그램의 코드가 저장되는 메모리 공간이다. 따라서 CPU는 코드 영역에 저장된 명령문들을 하나씩 가져가서 실행을 한다.
  • 데이터 영역 ( Data Area )
    데이터 영역에는 전역변수와 static으로 선언되는 static 변수가 할당된다. 즉, 이 영역에 할당되는 변수들은 프로그램의 시작과 동시에 메모리 공간에 할당되어 프로그램 종료 시까지 남아있게 된다는 특징이 있다.

  • 스택 영역 ( Stack Atrea )
    스택 영역에는 지역변수와 매개변수가 할당된다.
    이렇듯 이 영역에 할당되는 변수들은 선언된 함수를 빠져나가면 소멸된다는 특징이 있다.

  • 힙 영역 ( Heap Area )
    데이터 영역에 할당되는 변수와 스택 영역에 할당되는 변수들은 생성과 소멸의 시점이 이미 결정되어 있다.
    그러나 프로그램을 구현하다 보면, 이 두 영역의 변수들과는 다른 성격의 변수가 필요하기도 하다.
    그래서 C언어에서는 프로그래머가 원하는 시점에 변수를 할당하고 또 소멸하도록 지원을 하는데, 바로 이러한 유형의 변수들이 할당되는 영역이 힙 영역이다.

 













메모리의 동적 할당

동적 할당의 필요성

다음 예제에는 프로그램 사용자로부터 입력 받은 문자열의 정보를 반환하는 함수가 정의되어 있다.
이 함수의 문제점을 지적해보자.

#include <stdio.h>

char * ReadUserName(void){
	char name[30];
    printf("What's your name? ");
    gets(name);
    return name; // 무엇을 반환하는가?
}

int main(void){
	char * name1;
    char * name2;
    name1=ReadUserName();
    printf("name1 : %s \n", name1);
    name2=ReadUserName();
    printf("name2 : %s \n", name2);
    return 0;
}

위 예제의 문제점은 무엇인가?

그것은 함수 내에 지역적으로 선언된 배열(변수)의 주소 값을 반환하는데 있다. 함수 내에서 프로그램 사용자로부터 문자열을 직접 입력 받아서 그 결과를 반환하는 것은 좋다.
문제는 그 문자열이 저장되어 있는 배열이 지역적으로 선언되었기 때문에 함수를 빠져나오면서 소멸된다는데 있다. 그래서 실제로 실행을 해보면 정상적이지 못한 결과로 이어지는 것을 확인할 수 있다.

이럴 땐 함수가 매변 호출될 때마다 새롭게 할당되고 또 함수를 빠져나가도 유지가 되는 유형의 변수가 필요하다.
다시 말해서, 지역변수와 같이 함수가 호출될 때마다 매번 할당이 이뤄지지만, 할당이 되면 전역변수와 마찬가지로 함수를 빠져나가도 소멸되지 않는 성격의 변수가 필요하다.

그런데 다행스럽게도 이렇듯 생성과 소멸의 시기가 지역변구나 전역변수와 다른 유형의 변수는 malloc과 free 라는 이름의 함수를 통해서 힙 영역에 할당하고 소멸할 수 있다.

 





힙 영역의 메모리 공간 할당과 해제 : malloc과 free 함수

아래에서 보이는 malloc 함수를 이용해서 메모리 공간을 할당하고, 또 할당된 메모리 공간은 free 함수의 호출을 통해서 해제한다.

#include <stdlib.h>
void * malloc(size_t size);  // 힙 영역으로의 메모리 공간 할당
void free(void * ptr);      // 힙 영역에 할당된 메모리 공간 해제

// malloc 함수는 성공 시 할당된 메모리의 주소 값, 실패 시 NULL 반환

힙 영역을 흔히 '프로그래머가 관리하는 메모리 공간' 이라고 한다.
이유는 malloc 함수호출로 할당된 메모리 공간은 프로그래머가 직접 free 함수의 호출을 통해서 해제하지 않으면 계속 남아있기 때문이다.

즉 위의 두 함수는 다음과 같이 쌍을 이루어 호출하게 된다

int main(void){
	void * ptr1 = malloc(4);  // 4바이트가 힙 영역에 할당
    void * ptr2 = malloc(12); // 12바이트가 힙 영역에 할당
    . . . .
    free(ptr1); 	// ptr1이 가리키는 4바이트 메모리 공간 해제
    free(ptr2);		// ptr2가 가리키는 12바이트 메모리 공간 해제

이렇듯 malloc 함수는 인자로 전달된 정수 값에 해당하는 바이트 크기의 메모리 공간을 힙 영역에 할당하고, 이 메모리 공간의 주소 값을 반환한다.

 












malloc 함수의 반환형이 void형 포인터인 이유와 힙 영역으로의 접근

"힙에 할당된 메모리 공간은 포인터 변수를 이용해서 접근하는 방법밖에 없나?"

malloc 함수는 주소 값을 반환한다. 그리고 그 주소 값을 이용해서 힙에 접근을 해야 한다. 따라서 포인터를 이용해서 메모리 공간에 접근하는 수밖에 없다.
그러니 이제 malloc 함수의 반환형에 관심을 둬 보자.

malloc 함수의 반환형은 void형 포인터이다.
따라서 malloc 함수의 반환 값에 아무런 가공도 가하지 않으면, 이를 이용해서는 할당된 메모리 공간에 접근이 불가능하다.

void * ptr = malloc(sizeof(int)); // int형 변수 크기의 메모리 공간 할당
*ptr = 20; 	// ptr이 void형 포인터이므로 컴파일 에러

malloc 함수는 다음과 같이 말한다
"저를 원하시는 크기만큼 메모리 공간을 할당하고 그 메모리의 주소 값을 반환하겠습니다. 그러니 어떻게 사용할지는 포인터 형의 변환을 통해서 직접 결정하세요"

따라서 다음과 같이 void형으로 반환되는 주소 값을 적절히 형 변환해서 할당된 메모리 공간에 접근해야 한다.

int * ptr1 = (int *)malloc(sizeof(int));
double * ptr2 = (double *)malloc(sizeof(double));
int * ptr3 = (int *)malloc(sizeof(int)*7);
double * ptr4 = (double *)malloc(sizeof(double)*9);

그리고 malloc 함수의 호출을 통한 메모리 공간의 할당을 가리켜 '동적 할당(dynamic allocation)'이라 한다.
이유는 할당되는 메모리의 크기를 컴파일러가 결정하지 않고, 프로그램의 실행 중간에 호출되는 malloc 함수가 결정하기 때문이다.

 












calloc 함수

힙 영역에 메모리 공간을 할당하는 함수로 calloc이라는 함수가 다음과 같이 추가로 정의되어 있다.
아래의 함수와 malloc 함수의 유일한 차이점은 메모리 공간의 할당을 위한 인자의 전달방식에 있다.

#include <stdlib.h>
void * calloc(size_t elt_count, size_t elt_size);

-> 성공 시 할당된 메모리의 주소 값, 실패 시 NULL 반환

위의 함수 원형에서 보여주듯이 malloc 함수와 달리 calloc 함수는 두 개의 숫자를 인자로 전달받는다.
반면 앞서 보인 malloc 함수의 전달인자는 하나였다.
즉 malloc 함수의 호출방식은 다음과 같았다.
"총 120 바이트를 힙 영역에 할당해 주세요."

반면 calloc 함수의 첫 번째 전달인자로는 할당할 블록의 갯수 정보가 전달되고, 두 번째 전달인자로는 블록 하나당 바이트 크기의 정보가 전달된다.
즉 calloc 함수의 호출방식은 다음과 같다.
"4바이트 크기의 블록(elt_size) 30개를(elt_count) 힙 영역에 할당해 주세요"

120바이트를 할당해 달라는 것과, 4바이트 크기의 블록 30개를 할당해 달라는 것은 결과적으로 완전히 동일하다. 다시 말해서 calloc 함수는 malloc 함수와 인자를 전달하는 방식에서 차이를 보인다.

그런데 이것 말고도 한가지 차이점이 더 있다.
malloc 함수는 할당된 메모리 공간을 별도의 값으로 초기화하지 않는다.
따라서 할당된 메모리 공간이 쓰레기 값으로 채워지지만 calloc 함수는 할당된 메모리 공간의 모든 비트를 0으로 초기화시킨다.

바로 이러한 특성 때문에 calloc 함수가 대신 사용되는 경우도 많다.
그리고 calloc 함수의 호출로 할당된 메모리 공간을 해제할 때에도 malloc 함수와 마찬가지로 free 함수를 사용하면 된다.

 












힙에 할당된 메모리 공간 확장 시 호출하는 realloc 함수

한번 할당된 메모리 공간은 그 크리를 확장할 수 없다.
이는 모든 영역의 메모리 공간에 해당하는 말이다.

하지만 그 영역이 힙이라면, 그리고 realloc 함수를 사용한다면 이러한 일이 가능해진다.

#include <stdlib.h>
void * realloc(void * ptr, size_t size);

-> 성공 시 새로 할당된 메모리의 주소 값, 실패 시 NULL 반환

이 함수의 첫 번째 전달인자로, 확장하고자 하는 힙 메모리의 시작 주소 값을 전달한다.
그리고 두 번째 전달인자로는 확장하고자 하는 메모리의 전체 크기를 전달한다.

즉, 매개변수 ptr과 size를 이용해서 다음과 같은 요구를 할 때 호출하는 것이 realloc 함수이다.
"ptr이 가리키는 메모리의 크기를 size 크기로 조절해줘(늘려줘)(해줘)"

그리고 함수호출의 성공 시에는 새로 할당된 메모리의 주소 값이 반환되고, 실패 시에는 NULL이 반환된다. 즉, 위의 함수는 다음의 형태로 호출이 된다.

int main(void){
	int * arr = (int *)malloc(sizeof(int)*3);
    // 길이가 3인 int형 배열 할당
    . . . .
    arr = (int *)realloc(arr, sizeof(int)*5);
    // 길이가 5인 int형 배열로 확장
    . . . . 
}

위 코드의 실행결과는 반환 값을 기준으로 다음과 같이 두가지로 구분이 된다.
"malloc 함수가 반환한 주소 값과 realloc 함수가 반환한 주소 값이 같은 경우"
"malloc 함수가 반환한 주소 값과 realloc 함수가 반환한 주소 값이 같지 않은 경우"

전자는 기존에 할당된 메모리 공간의 뒤를 이어서, 확장할 영역이 넉넉한 경우에 발생한다.
하지만 넉넉하지 않은 경우에는 힙의 다른 위치에, 새로이 요구하는 크기의 메모리 공간을 별도로 할당해서 이전 배열에 저장된 값을 복사하기도 한다.
그리고 이러한 경우에는 후자의 경우와 같이 malloc 함수와 realloc 함수의 반환 값이 같지 않다.

0개의 댓글